From 93b7cd83703e684fd7ea52d7004f7a3ce6936d9e Mon Sep 17 00:00:00 2001 From: crgisch Date: Mon, 19 Aug 2024 11:48:47 -0300 Subject: [PATCH] test: add support to deploy job with Dockerfile --- pkg/build/buildkit/build_test.go | 240 +++++++++++++++++++++++++++++++ pkg/build/server_test.go | 56 ++++++++ 2 files changed, 296 insertions(+) diff --git a/pkg/build/buildkit/build_test.go b/pkg/build/buildkit/build_test.go index 780249e..bec70b5 100644 --- a/pkg/build/buildkit/build_test.go +++ b/pkg/build/buildkit/build_test.go @@ -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 diff --git a/pkg/build/server_test.go b/pkg/build/server_test.go index 3512c48..27c6240 100644 --- a/pkg/build/server_test.go +++ b/pkg/build/server_test.go @@ -127,6 +127,20 @@ func TestBuild(t *testing.T) { }, }, + "missing job, when kind is job dockerfile": { + req: &pb.BuildRequest{ + SourceImage: "tsuru/scratch:latest", + DestinationImages: []string{"registry.example.com/tsuru/app-my-app:v1"}, + Kind: pb.BuildKind_BUILD_KIND_JOB_DEPLOY_WITH_CONTAINER_IMAGE, + }, + assert: func(t *testing.T, stream pb.Build_BuildClient, err error) { + require.NoError(t, err) + require.NotNil(t, stream) + _, _, err = readResponse(t, stream) + assert.EqualError(t, err, status.Error(codes.InvalidArgument, "job cannot be nil").Error()) + }, + }, + "deploy from source code, empty source image": { req: &pb.BuildRequest{ DestinationImages: []string{"registry.example.com/tsuru/app-my-app:v1"}, @@ -208,6 +222,33 @@ func TestBuild(t *testing.T) { }, }, + "job build successful": { + builder: &fake.FakeBuilder{ + OnBuild: func(ctx context.Context, r *pb.BuildRequest, w io.Writer) (*pb.TsuruConfig, error) { + assert.NotNil(t, ctx) + assert.NotNil(t, r) + assert.NotNil(t, w) + fmt.Fprintln(w, "--- EXECUTING BUILD ---") + return nil, nil + }, + }, + req: &pb.BuildRequest{ + Containerfile: "FROM tsuru/scratch:latest", + DestinationImages: []string{"registry.example.com/tsuru/job-my-job:latest"}, + Kind: pb.BuildKind_BUILD_KIND_JOB_DEPLOY_WITH_CONTAINER_FILE, + Job: &pb.TsuruJob{Name: "my-job"}, + Data: []byte("fake data :P"), + }, + assert: func(t *testing.T, stream pb.Build_BuildClient, err error) { + require.NoError(t, err) + require.NotNil(t, stream) + tsuruConfig, output, err := readResponse(t, stream) + require.NoError(t, err) + require.Nil(t, tsuruConfig) + assert.Regexp(t, `(.*)--- EXECUTING BUILD ---(.*)`, output) + }, + }, + "platform build, missing platform": { req: &pb.BuildRequest{ DestinationImages: []string{"registry.example.com/tsuru/app-my-app:v1"}, @@ -275,6 +316,21 @@ func TestBuild(t *testing.T) { assert.EqualError(t, err, status.Error(codes.InvalidArgument, "containerfile cannot be empty").Error()) }, }, + + "job deploy with containerfile, empty containerfile": { + req: &pb.BuildRequest{ + SourceImage: "...", + DestinationImages: []string{"registry.example.com/tsuru/job-my-job:latest"}, + Job: &pb.TsuruJob{Name: "my-job"}, + Kind: pb.BuildKind_BUILD_KIND_JOB_DEPLOY_WITH_CONTAINER_FILE, + }, + assert: func(t *testing.T, stream pb.Build_BuildClient, err error) { + require.NoError(t, err) + require.NotNil(t, stream) + _, _, err = readResponse(t, stream) + assert.EqualError(t, err, status.Error(codes.InvalidArgument, "containerfile cannot be empty").Error()) + }, + }, } for name, tt := range cases {