Skip to content

Commit 8e2563d

Browse files
committed
Use a minimal container for GOLANG/CAR chaincode
This patch implements a "pass-through" build of chaincode within a docker container as an alternative to using standard "docker build" + Dockerfile mechanisms. The plain docker build is somewhat limiting due to the resulting image that is a superset composition of the build-time and run-time environments. This superset can be problematic on several fronts, such as a bloated image size, and additional security exposure associated with applications that are not needed, etc. Before this patch, chaincode containers were approximately 1.4GB. After this patch, they are about 175MB. Java chaincode has not been ported to this process Change-Id: Ib4e9881a0c1bfd6fb2f098e105a61d82c5293b3d Signed-off-by: Greg Haskins <[email protected]>
1 parent 1b7af6d commit 8e2563d

File tree

7 files changed

+204
-55
lines changed

7 files changed

+204
-55
lines changed

core/chaincode/chaincodetest.yaml

+8-11
Original file line numberDiff line numberDiff line change
@@ -349,20 +349,17 @@ chaincode:
349349
path:
350350
name:
351351

352-
golang:
352+
# Generic builder environment, suitable for most chaincode types
353+
builder: hyperledger/fabric-ccenv:$(ARCH)-$(PROJECT_VERSION)
353354

354-
# This is the basis for the Golang Dockerfile. Additional commands will
355-
# be appended depedendent upon the chaincode specification.
356-
Dockerfile: |
357-
FROM hyperledger/fabric-ccenv:$(ARCH)-$(PROJECT_VERSION)
358-
WORKDIR $GOPATH
355+
golang:
356+
# golang will never need more than baseos
357+
runtime: hyperledger/fabric-baseos:$(ARCH)-$(BASE_VERSION)
359358

360359
car:
361-
362-
# This is the basis for the CAR Dockerfile. Additional commands will
363-
# be appended depedendent upon the chaincode specification.
364-
Dockerfile: |
365-
FROM hyperledger/fabric-ccenv:$(ARCH)-$(PROJECT_VERSION)
360+
# car may need more facilities (JVM, etc) in the future as the catalog
361+
# of platforms are expanded. For now, we can just use baseos
362+
runtime: hyperledger/fabric-baseos:$(ARCH)-$(BASE_VERSION)
366363

367364
# timeout in millisecs for starting up a container and waiting for Register
368365
# to come through. 1sec should be plenty for chaincode unit tests

core/chaincode/platforms/car/platform.go

+29-5
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,11 @@ import (
2121
"io/ioutil"
2222
"strings"
2323

24+
"bytes"
25+
"fmt"
26+
"io"
27+
28+
"github.com/hyperledger/fabric/core/chaincode/platforms/util"
2429
cutil "github.com/hyperledger/fabric/core/container/util"
2530
pb "github.com/hyperledger/fabric/protos/peer"
2631
)
@@ -51,10 +56,8 @@ func (carPlatform *Platform) GenerateDockerfile(cds *pb.ChaincodeDeploymentSpec)
5156
var buf []string
5257

5358
//let the executable's name be chaincode ID's name
54-
buf = append(buf, cutil.GetDockerfileFromConfig("chaincode.car.Dockerfile"))
55-
buf = append(buf, "COPY codepackage.car /tmp/codepackage.car")
56-
// invoking directly for maximum JRE compatiblity
57-
buf = append(buf, "RUN java -jar /usr/local/bin/chaintool buildcar /tmp/codepackage.car -o /usr/local/bin/chaincode && rm /tmp/codepackage.car")
59+
buf = append(buf, "FROM "+cutil.GetDockerfileFromConfig("chaincode.car.runtime"))
60+
buf = append(buf, "ADD binpackage.tar /usr/local/bin")
5861

5962
dockerFileContents := strings.Join(buf, "\n")
6063

@@ -63,5 +66,26 @@ func (carPlatform *Platform) GenerateDockerfile(cds *pb.ChaincodeDeploymentSpec)
6366

6467
func (carPlatform *Platform) GenerateDockerBuild(cds *pb.ChaincodeDeploymentSpec, tw *tar.Writer) error {
6568

66-
return cutil.WriteBytesToPackage("codepackage.car", cds.CodePackage, tw)
69+
// Bundle the .car file into a tar stream so it may be transferred to the builder container
70+
codepackage, output := io.Pipe()
71+
go func() {
72+
tw := tar.NewWriter(output)
73+
74+
err := cutil.WriteBytesToPackage("codepackage.car", cds.CodePackage, tw)
75+
76+
tw.Close()
77+
output.CloseWithError(err)
78+
}()
79+
80+
binpackage := bytes.NewBuffer(nil)
81+
err := util.DockerBuild(util.DockerBuildOptions{
82+
Cmd: "java -jar /usr/local/bin/chaintool buildcar /chaincode/input/codepackage.car -o /chaincode/output/chaincode",
83+
InputStream: codepackage,
84+
OutputStream: binpackage,
85+
})
86+
if err != nil {
87+
return fmt.Errorf("Error building CAR: %s", err)
88+
}
89+
90+
return cutil.WriteBytesToPackage("binpackage.tar", binpackage.Bytes(), tw)
6791
}

core/chaincode/platforms/golang/platform.go

+24-17
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import (
2929

3030
"regexp"
3131

32+
"github.com/hyperledger/fabric/core/chaincode/platforms/util"
3233
cutil "github.com/hyperledger/fabric/core/container/util"
3334
pb "github.com/hyperledger/fabric/protos/peer"
3435
)
@@ -186,29 +187,35 @@ func (goPlatform *Platform) GetDeploymentPayload(spec *pb.ChaincodeSpec) ([]byte
186187
func (goPlatform *Platform) GenerateDockerfile(cds *pb.ChaincodeDeploymentSpec) (string, error) {
187188

188189
var buf []string
189-
var err error
190-
191-
spec := cds.ChaincodeSpec
192-
193-
urlLocation, err := decodeUrl(spec)
194-
if err != nil {
195-
return "", fmt.Errorf("could not decode url: %s", err)
196-
}
197190

198-
const env = "GOPATH=/tmp/codepackage:$GOPATH"
199-
const flags = "-ldflags \"-linkmode external -extldflags '-static'\""
200-
201-
buf = append(buf, cutil.GetDockerfileFromConfig("chaincode.golang.Dockerfile"))
202-
buf = append(buf, "ADD codepackage.tgz /tmp/codepackage")
203-
//let the executable's name be chaincode ID's name
204-
buf = append(buf, fmt.Sprintf("RUN %s go build %s -o /usr/local/bin/chaincode %s", env, flags, urlLocation))
205-
buf = append(buf, "RUN rm -rf /tmp/codepackage") // FAB-2122: scrub source after it is no longer needed
191+
buf = append(buf, "FROM "+cutil.GetDockerfileFromConfig("chaincode.golang.runtime"))
192+
buf = append(buf, "ADD binpackage.tar /usr/local/bin")
206193

207194
dockerFileContents := strings.Join(buf, "\n")
208195

209196
return dockerFileContents, nil
210197
}
211198

212199
func (goPlatform *Platform) GenerateDockerBuild(cds *pb.ChaincodeDeploymentSpec, tw *tar.Writer) error {
213-
return cutil.WriteBytesToPackage("codepackage.tgz", cds.CodePackage, tw)
200+
spec := cds.ChaincodeSpec
201+
202+
pkgname, err := decodeUrl(spec)
203+
if err != nil {
204+
return fmt.Errorf("could not decode url: %s", err)
205+
}
206+
207+
const ldflags = "-linkmode external -extldflags '-static'"
208+
209+
codepackage := bytes.NewReader(cds.CodePackage)
210+
binpackage := bytes.NewBuffer(nil)
211+
err = util.DockerBuild(util.DockerBuildOptions{
212+
Cmd: fmt.Sprintf("GOPATH=/chaincode/input:$GOPATH go build -ldflags \"%s\" -o /chaincode/output/chaincode %s", ldflags, pkgname),
213+
InputStream: codepackage,
214+
OutputStream: binpackage,
215+
})
216+
if err != nil {
217+
return err
218+
}
219+
220+
return cutil.WriteBytesToPackage("binpackage.tar", binpackage.Bytes(), tw)
214221
}

core/chaincode/platforms/util/utils.go

+126
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ import (
88
"os"
99
"path/filepath"
1010

11+
"io"
12+
13+
"github.com/fsouza/go-dockerclient"
1114
"github.com/hyperledger/fabric/common/util"
1215
cutil "github.com/hyperledger/fabric/core/container/util"
1316
"github.com/op/go-logging"
@@ -90,3 +93,126 @@ func IsCodeExist(tmppath string) error {
9093

9194
return nil
9295
}
96+
97+
type DockerBuildOptions struct {
98+
Image string
99+
Env []string
100+
Cmd string
101+
InputStream io.Reader
102+
OutputStream io.Writer
103+
}
104+
105+
//-------------------------------------------------------------------------------------------
106+
// DockerBuild
107+
//-------------------------------------------------------------------------------------------
108+
// This function allows a "pass-through" build of chaincode within a docker container as
109+
// an alternative to using standard "docker build" + Dockerfile mechanisms. The plain docker
110+
// build is somewhat limiting due to the resulting image that is a superset composition of
111+
// the build-time and run-time environments. This superset can be problematic on several
112+
// fronts, such as a bloated image size, and additional security exposure associated with
113+
// applications that are not needed, etc.
114+
//
115+
// Therefore, this mechanism creates a pipeline consisting of an ephemeral docker
116+
// container that accepts source code as input, runs some function (e.g. "go build"), and
117+
// outputs the result. The intention is that this output will be consumed as the basis of
118+
// a streamlined container by installing the output into a downstream docker-build based on
119+
// an appropriate minimal image.
120+
//
121+
// The input parameters are fairly simple:
122+
// - Image: (optional) The builder image to use or "chaincode.builder"
123+
// - Env: (optional) environment variables for the build environment.
124+
// - Cmd: The command to execute inside the container.
125+
// - InputStream: A tarball of files that will be expanded into /chaincode/input.
126+
// - OutputStream: A tarball of files that will be gathered from /chaincode/output
127+
// after successful execution of Cmd.
128+
//-------------------------------------------------------------------------------------------
129+
func DockerBuild(opts DockerBuildOptions) error {
130+
client, err := cutil.NewDockerClient()
131+
if err != nil {
132+
return fmt.Errorf("Error creating docker client: %s", err)
133+
}
134+
135+
if opts.Image == "" {
136+
opts.Image = cutil.GetDockerfileFromConfig("chaincode.builder")
137+
if opts.Image == "" {
138+
return fmt.Errorf("No image provided and \"chaincode.builder\" default does not exist")
139+
}
140+
}
141+
142+
//-----------------------------------------------------------------------------------
143+
// Create an ephemeral container, armed with our Env/Cmd
144+
//-----------------------------------------------------------------------------------
145+
container, err := client.CreateContainer(docker.CreateContainerOptions{
146+
Config: &docker.Config{
147+
Image: opts.Image,
148+
Env: opts.Env,
149+
Cmd: []string{"/bin/sh", "-c", opts.Cmd},
150+
AttachStdout: true,
151+
AttachStderr: true,
152+
},
153+
})
154+
if err != nil {
155+
return fmt.Errorf("Error creating container: %s", err)
156+
}
157+
defer client.RemoveContainer(docker.RemoveContainerOptions{ID: container.ID})
158+
159+
//-----------------------------------------------------------------------------------
160+
// Upload our input stream
161+
//-----------------------------------------------------------------------------------
162+
err = client.UploadToContainer(container.ID, docker.UploadToContainerOptions{
163+
Path: "/chaincode/input",
164+
InputStream: opts.InputStream,
165+
})
166+
if err != nil {
167+
return fmt.Errorf("Error uploading input to container: %s", err)
168+
}
169+
170+
//-----------------------------------------------------------------------------------
171+
// Attach stdout buffer to capture possible compilation errors
172+
//-----------------------------------------------------------------------------------
173+
stdout := bytes.NewBuffer(nil)
174+
_, err = client.AttachToContainerNonBlocking(docker.AttachToContainerOptions{
175+
Container: container.ID,
176+
OutputStream: stdout,
177+
ErrorStream: stdout,
178+
Logs: true,
179+
Stdout: true,
180+
Stderr: true,
181+
Stream: true,
182+
})
183+
if err != nil {
184+
return fmt.Errorf("Error attaching to container: %s", err)
185+
}
186+
187+
//-----------------------------------------------------------------------------------
188+
// Launch the actual build, realizing the Env/Cmd specified at container creation
189+
//-----------------------------------------------------------------------------------
190+
err = client.StartContainer(container.ID, nil)
191+
if err != nil {
192+
return fmt.Errorf("Error executing build: %s \"%s\"", err, stdout.String())
193+
}
194+
195+
//-----------------------------------------------------------------------------------
196+
// Wait for the build to complete and gather the return value
197+
//-----------------------------------------------------------------------------------
198+
retval, err := client.WaitContainer(container.ID)
199+
if err != nil {
200+
return fmt.Errorf("Error waiting for container to complete: %s", err)
201+
}
202+
if retval > 0 {
203+
return fmt.Errorf("Error returned from build: %d \"%s\"", retval, stdout.String())
204+
}
205+
206+
//-----------------------------------------------------------------------------------
207+
// Finally, download the result
208+
//-----------------------------------------------------------------------------------
209+
err = client.DownloadFromContainer(container.ID, docker.DownloadFromContainerOptions{
210+
Path: "/chaincode/output/.",
211+
OutputStream: opts.OutputStream,
212+
})
213+
if err != nil {
214+
return fmt.Errorf("Error downloading output: %s", err)
215+
}
216+
217+
return nil
218+
}

core/endorser/endorser_test.yaml

+8-11
Original file line numberDiff line numberDiff line change
@@ -363,20 +363,17 @@ chaincode:
363363
path:
364364
name:
365365

366-
golang:
366+
# Generic builder environment, suitable for most chaincode types
367+
builder: hyperledger/fabric-ccenv:$(ARCH)-$(PROJECT_VERSION)
367368

368-
# This is the basis for the Golang Dockerfile. Additional commands will
369-
# be appended depedendent upon the chaincode specification.
370-
Dockerfile: |
371-
FROM hyperledger/fabric-ccenv:$(ARCH)-$(PROJECT_VERSION)
372-
WORKDIR $GOPATH
369+
golang:
370+
# golang will never need more than baseos
371+
runtime: hyperledger/fabric-baseos:$(ARCH)-$(BASE_VERSION)
373372

374373
car:
375-
376-
# This is the basis for the CAR Dockerfile. Additional commands will
377-
# be appended depedendent upon the chaincode specification.
378-
Dockerfile: |
379-
FROM hyperledger/fabric-ccenv:$(ARCH)-$(PROJECT_VERSION)
374+
# car may need more facilities (JVM, etc) in the future as the catalog
375+
# of platforms are expanded. For now, we can just use baseos
376+
runtime: hyperledger/fabric-baseos:$(ARCH)-$(BASE_VERSION)
380377

381378
java:
382379
# This is an image based on java:openjdk-8 with addition compiler

images/ccenv/Dockerfile.in

+1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
FROM hyperledger/fabric-baseimage:_BASE_TAG_
22
COPY payload/chaintool payload/protoc-gen-go /usr/local/bin/
33
ADD payload/goshim.tar.bz2 $GOPATH/src/
4+
RUN mkdir -p /chaincode/input /chaincode/output

peer/core.yaml

+8-11
Original file line numberDiff line numberDiff line change
@@ -276,20 +276,17 @@ chaincode:
276276
path:
277277
name:
278278

279-
golang:
279+
# Generic builder environment, suitable for most chaincode types
280+
builder: hyperledger/fabric-ccenv:$(ARCH)-$(PROJECT_VERSION)
280281

281-
# This is the basis for the Golang Dockerfile. Additional commands will
282-
# be appended depedendent upon the chaincode specification.
283-
Dockerfile: |
284-
FROM hyperledger/fabric-ccenv:$(ARCH)-$(PROJECT_VERSION)
285-
WORKDIR $GOPATH
282+
golang:
283+
# golang will never need more than baseos
284+
runtime: hyperledger/fabric-baseos:$(ARCH)-$(BASE_VERSION)
286285

287286
car:
288-
289-
# This is the basis for the CAR Dockerfile. Additional commands will
290-
# be appended depedendent upon the chaincode specification.
291-
Dockerfile: |
292-
FROM hyperledger/fabric-ccenv:$(ARCH)-$(PROJECT_VERSION)
287+
# car may need more facilities (JVM, etc) in the future as the catalog
288+
# of platforms are expanded. For now, we can just use baseos
289+
runtime: hyperledger/fabric-baseos:$(ARCH)-$(BASE_VERSION)
293290

294291
java:
295292
# This is an image based on java:openjdk-8 with addition compiler

0 commit comments

Comments
 (0)