Skip to content

Commit

Permalink
test: add a bom test for derived images (#483)
Browse files Browse the repository at this point in the history
* test: add a bom test for derived images

Signed-off-by: Ramkumar Chinchani <rchincha@cisco.com>

* fix: refactor code and also upload empty config blob

Signed-off-by: Ramkumar Chinchani <rchincha@cisco.com>

---------

Signed-off-by: Ramkumar Chinchani <rchincha@cisco.com>
  • Loading branch information
rchincha authored Sep 5, 2023
1 parent e721fe0 commit fdddd5b
Show file tree
Hide file tree
Showing 5 changed files with 208 additions and 57 deletions.
4 changes: 4 additions & 0 deletions .github/workflows/build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -66,11 +66,15 @@ jobs:
make stacker VERSION_FULL=${{ matrix.build-id }}
env:
REGISTRY_URL: localhost:5000
ZOT_HOST: localhost
ZOT_PORT: 8080
- name: Test
run: |
make check VERSION_FULL=${{ matrix.build-id }} PRIVILEGE_LEVEL=${{ matrix.privilege-level }}
env:
REGISTRY_URL: localhost:5000
ZOT_HOST: localhost
ZOT_PORT: 8080
- name: Upload code coverage
uses: codecov/codecov-action@v3
- uses: actions/cache@v3
Expand Down
20 changes: 19 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,14 @@ STACKER_BUILD_UBUNTU_IMAGE?=$(STACKER_DOCKER_BASE)ubuntu:latest
LXC_CLONE_URL?=https://github.com/lxc/lxc
LXC_BRANCH?=stable-5.0

# helper tools
TOOLSDIR := $(shell pwd)/hack/tools
REGCLIENT := $(TOOLSDIR)/bin/regctl
REGCLIENT_VERSION := v0.5.1
# OCI registry
ZOT := $(TOOLSDIR)/bin/zot
ZOT_VERSION := 2.0.0-rc6

STAGE1_STACKER ?= ./stacker-dynamic

STACKER_DEPS = $(GO_SRC) go.mod go.sum
Expand Down Expand Up @@ -58,13 +66,23 @@ lint: cmd/stacker/lxc-wrapper/lxc-wrapper $(GO_SRC)
go test -v -trimpath -cover -coverpkg stackerbuild.io/stacker/./... -coverprofile=coverage.txt -covermode=atomic -tags "$(BUILD_TAGS)" stackerbuild.io/stacker/./...
$(shell go env GOPATH)/bin/golangci-lint run --build-tags "$(BUILD_TAGS)"

$(REGCLIENT):
mkdir -p $(TOOLSDIR)/bin
curl -Lo $(REGCLIENT) https://github.com/regclient/regclient/releases/download/$(REGCLIENT_VERSION)/regctl-linux-amd64
chmod +x $(REGCLIENT)

$(ZOT):
mkdir -p $(TOOLSDIR)/bin
curl -Lo $(ZOT) https://github.com/project-zot/zot/releases/download/v$(ZOT_VERSION)/zot-linux-amd64-minimal
chmod +x $(ZOT)

TEST?=$(patsubst test/%.bats,%,$(wildcard test/*.bats))
PRIVILEGE_LEVEL?=

# make check TEST=basic will run only the basic test
# make check PRIVILEGE_LEVEL=unpriv will run only unprivileged tests
.PHONY: check
check: stacker lint
check: stacker lint $(REGCLIENT) $(ZOT)
sudo -E PATH="$$PATH" \
LXC_BRANCH=$(LXC_BRANCH) \
LXC_CLONE_URL=$(LXC_CLONE_URL) \
Expand Down
150 changes: 100 additions & 50 deletions pkg/stacker/publisher.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,10 +125,12 @@ func fileDigest(path string) (*godigest.Digest, error) {
}

// publishArtifact to a registry/repo for this subject
func (p *Publisher) publishArtifact(path, mtype, registry, repo, subject string, skipTLS bool) error {
func (p *Publisher) publishArtifact(path, mtype, registry, repo, subjectTag string, skipTLS bool) error {
username := p.opts.Username
password := p.opts.Password

subject := distspecURL(registry, repo, subjectTag, skipTLS)

// check subject exists
res, err := clientRequest(http.MethodHead, subject, username, password, nil, nil)
if err != nil {
Expand Down Expand Up @@ -160,56 +162,26 @@ func (p *Publisher) publishArtifact(path, mtype, registry, repo, subject string,
return err
}

// upload with POST, PUT sequence
var regUrl string
if skipTLS {
regUrl = fmt.Sprintf("http://%s/v2%s/blobs/uploads/", registry, strings.Split(repo, ":")[0])
} else {
regUrl = fmt.Sprintf("https://%s/v2%s/blobs/uploads/", registry, strings.Split(repo, ":")[0])
}
log.Debugf("new blob upload (POST): %s", regUrl)
res, err = clientRequest(http.MethodPost, regUrl, username, password, nil, nil)
if err != nil {
log.Errorf("post unable to check subject:%s, err:%s", subject, err)
return err
}
log.Debugf("http response headers: +%v status:%v", res.Header, res.Status)
loc, err := res.Location()
if err != nil {
log.Errorf("unable get upload location url:%s, err:%s", regUrl, err)
return err
}

fh, err := os.Open(path)
if err != nil {
log.Errorf("unable to open file:%s, err:%s", path, err)
return err
}
defer fh.Close()

log.Debugf("finish blob upload (PUT): %s", regUrl)
req, err := http.NewRequestWithContext(context.TODO(), http.MethodPut, loc.String(), fh)
if err != nil {
log.Errorf("unable to create a http request url:%s", subject)
if err := uploadBlob(registry, repo, path, username, password, fh, finfo.Size(), dgst, skipTLS); err != nil {
log.Errorf("unable to upload file:%s, err:%s", path, err)
return err
}
if username != "" && password != "" {
req.Header.Add("Authorization", "Basic "+basicAuth(username, password))
}
req.URL.RawQuery = url.Values{
"digest": {dgst.String()},
}.Encode()
req.ContentLength = finfo.Size()

res, err = http.DefaultClient.Do(req)
if err != nil {
log.Errorf("http request failed url:%s", subject)
// check and upload emptyJSON blob
erdr := bytes.NewReader(ispec.DescriptorEmptyJSON.Data)
edgst := ispec.DescriptorEmptyJSON.Digest

if err := uploadBlob(registry, repo, path, username, password, erdr, ispec.DescriptorEmptyJSON.Size, &edgst, skipTLS); err != nil {
log.Errorf("unable to upload file:%s, err:%s", path, err)
return err
}
if res == nil || res.StatusCode != http.StatusCreated {
log.Errorf("unable to upload artifact:%s to url:%s", path, regUrl)
return errors.Errorf("unable to upload artifact:%s to url:%s", path, regUrl)
}

// upload the reference manifest
manifest := ispec.Manifest{
Expand Down Expand Up @@ -241,6 +213,7 @@ func (p *Publisher) publishArtifact(path, mtype, registry, repo, subject string,
}

// artifact manifest
var regUrl string
mdgst := godigest.FromBytes(content)
if skipTLS {
regUrl = fmt.Sprintf("http://%s/v2%s/manifests/%s", registry, strings.Split(repo, ":")[0], mdgst.String())
Expand All @@ -261,7 +234,92 @@ func (p *Publisher) publishArtifact(path, mtype, registry, repo, subject string,
return errors.Errorf("unable to upload manifest, url:%s", regUrl)
}

log.Infof("artifact '%s' sucessfully uploaded to url:%s", path, regUrl)
log.Infof("Copying artifact '%s' done", path)

return nil
}

func distspecURL(registry, repo, tag string, skipTLS bool) string {
var url string

if skipTLS {
url = fmt.Sprintf("http://%s/v2%s/manifests/%s", registry, strings.Split(repo, ":")[0], tag)
} else {
url = fmt.Sprintf("https://%s/v2%s/manifests/%s", registry, strings.Split(repo, ":")[0], tag)
}

return url
}

func uploadBlob(registry, repo, path, username, password string, reader io.Reader, size int64, dgst *godigest.Digest, skipTLS bool) error {
// upload with POST, PUT sequence
var regUrl string
if skipTLS {
regUrl = fmt.Sprintf("http://%s/v2%s/blobs/%s", registry, strings.Split(repo, ":")[0], dgst.String())
} else {
regUrl = fmt.Sprintf("https://%s/v2%s/blobs/%s", registry, strings.Split(repo, ":")[0], dgst.String())
}

subject := distspecURL(registry, repo, "", skipTLS)

log.Debugf("check blob before upload (HEAD): %s", regUrl)
res, err := clientRequest(http.MethodHead, regUrl, username, password, nil, nil)
if err != nil {
log.Errorf("unable to check blob:%s, err:%s", subject, err)
return err
}
log.Debugf("http response headers: +%v status:%v", res.Header, res.Status)
hdr := res.Header.Get("Docker-Content-Digest")
if hdr != "" {
log.Infof("Copying blob %s skipped: already exists", dgst.Hex()[:12])
return nil
}

if skipTLS {
regUrl = fmt.Sprintf("http://%s/v2%s/blobs/uploads/", registry, strings.Split(repo, ":")[0])
} else {
regUrl = fmt.Sprintf("https://%s/v2%s/blobs/uploads/", registry, strings.Split(repo, ":")[0])
}

log.Debugf("new blob upload (POST): %s", regUrl)
res, err = clientRequest(http.MethodPost, regUrl, username, password, nil, nil)
if err != nil {
log.Errorf("post unable to check subject:%s, err:%s", subject, err)
return err
}
log.Debugf("http response headers: +%v status:%v", res.Header, res.Status)
loc, err := res.Location()
if err != nil {
log.Errorf("unable get upload location url:%s, err:%s", regUrl, err)
return err
}

log.Debugf("finish blob upload (PUT): %s", regUrl)
req, err := http.NewRequestWithContext(context.TODO(), http.MethodPut, loc.String(), reader)
if err != nil {
log.Errorf("unable to create a http request url:%s", subject)
return err
}
if username != "" && password != "" {
req.Header.Add("Authorization", "Basic "+basicAuth(username, password))
}
req.URL.RawQuery = url.Values{
"digest": {dgst.String()},
}.Encode()

req.ContentLength = size

res, err = http.DefaultClient.Do(req)
if err != nil {
log.Errorf("http request failed url:%s", subject)
return err
}
if res == nil || res.StatusCode != http.StatusCreated {
log.Errorf("unable to upload artifact:%s to url:%s", path, regUrl)
return errors.Errorf("unable to upload artifact:%s to url:%s", path, regUrl)
}

log.Infof("Copying blob %s done", dgst.Hex()[:12])

return nil
}
Expand Down Expand Up @@ -393,23 +451,15 @@ func (p *Publisher) Publish(file string) error {
registry := url.Host
repo := url.Path

var subject string

if opts.SkipTLS {
subject = fmt.Sprintf("http://%s/v2%s/manifests/%s", registry, strings.Split(repo, ":")[0], layerTypeTag)
} else {
subject = fmt.Sprintf("https://%s/v2%s/manifests/%s", registry, strings.Split(repo, ":")[0], layerTypeTag)
}

// publish sbom
if err := p.publishArtifact(path.Join(opts.Config.StackerDir, "artifacts", layerName, fmt.Sprintf("%s.json", layerName)),
"application/spdx+json", registry, repo, subject, opts.SkipTLS); err != nil {
"application/spdx+json", registry, repo, layerTypeTag, opts.SkipTLS); err != nil {
return err
}

// publish inventory
if err := p.publishArtifact(path.Join(opts.Config.StackerDir, "artifacts", layerName, "inventory.json"),
"application/vnd.stackerbuild.inventory+json", registry, repo, subject, opts.SkipTLS); err != nil {
"application/vnd.stackerbuild.inventory+json", registry, repo, layerTypeTag, opts.SkipTLS); err != nil {
return err
}

Expand Down
42 changes: 36 additions & 6 deletions test/bom.bats
Original file line number Diff line number Diff line change
Expand Up @@ -106,19 +106,49 @@ bom-parent:
org.opencontainers.image.authors: "Alice P. Programmer"
org.opencontainers.image.vendor: "ACME Widgets & Trinkets Inc."
org.opencontainers.image.licenses: MIT
bom-child:
from:
type: built
tag: bom-parent
bom:
generate: true
packages:
- name: pkg3
version: 1.0.0
license: Apache-2.0
paths: [/pkg3]
run: |
# our own custom packages
mkdir -p /pkg3
touch /pkg3/file
annotations:
org.opencontainers.image.authors: bom-test
org.opencontainers.image.vendor: bom-test
org.opencontainers.image.licenses: MIT
EOF
stacker build
[ -f .stacker/artifacts/bom-parent/installed-packages.json ]
# a full inventory for this image
[ -f .stacker/artifacts/bom-parent/inventory.json ]
# sbom for this image
[ -f .stacker/artifacts/bom-parent/bom-parent.json ]
if [ -nz "${REGISTRY_URL}" ]; then
stacker publish --skip-tls --url docker://localhost:8080/ --tag latest
refs=$(regctl artifact tree localhost:8080/bom-parent:latest --format "{{json .}}" | jq '.referrer | length')
[ $refs eq 2 ]
refs=$(regctl artifact tree localhost:8080/bom-parent:latest --filter-artifact-type "application/spdx+json" --format "{{json .}}" | jq '.SPDXID')
[ $refs eq "SPDXRef-DOCUMENT" ]
# a full inventory for this image
[ -f .stacker/artifacts/bom-child/inventory.json ]
# sbom for this image
[ -f .stacker/artifacts/bom-child/bom-child.json ]
if [ -n "${ZOT_HOST}:${ZOT_PORT}" ]; then
zot_setup
stacker publish --skip-tls --url docker://${ZOT_HOST}:${ZOT_PORT} --tag latest
refs=$(regctl artifact tree ${ZOT_HOST}:${ZOT_PORT}/bom-parent:latest --format "{{json .}}" | jq '.referrer | length')
[ $refs -eq 2 ]
refs=$(regctl artifact get --subject ${ZOT_HOST}:${ZOT_PORT}/bom-parent:latest --filter-artifact-type "application/spdx+json" | jq '.SPDXID')
[ $refs == \"SPDXRef-DOCUMENT\" ]
refs=$(regctl artifact tree ${ZOT_HOST}:${ZOT_PORT}/bom-child:latest --format "{{json .}}" | jq '.referrer | length')
[ $refs -eq 2 ]
refs=$(regctl artifact get --subject ${ZOT_HOST}:${ZOT_PORT}/bom-child:latest --filter-artifact-type "application/spdx+json" | jq '.SPDXID')
[ $refs == \"SPDXRef-DOCUMENT\" ]
zot_teardown
fi
stacker clean
}
Expand Down
49 changes: 49 additions & 0 deletions test/helpers.bash
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ STACKER_BUILD_UBUNTU_IMAGE=${STACKER_BUILD_UBUNTU_IMAGE:-${STACKER_DOCKER_BASE}u
) 9<$ROOT_DIR/test/main.py
export CENTOS_OCI="$ROOT_DIR/test/centos:latest"
export UBUNTU_OCI="$ROOT_DIR/test/ubuntu:latest"
export PATH="$PATH:$ROOT_DIR/hack/tools/bin"

function sha() {
echo $(sha256sum $1 | cut -f1 -d" ")
Expand Down Expand Up @@ -141,3 +142,51 @@ function cmp_files() {
fi
return 0
}

function zot_setup {
cat > $TEST_TMPDIR/zot-config.json << EOF
{
"distSpecVersion": "1.1.0-dev",
"storage": {
"rootDirectory": "$TEST_TMPDIR/zot",
"gc": true,
"dedupe": true
},
"http": {
"address": "$ZOT_HOST",
"port": "$ZOT_PORT"
},
"log": {
"level": "error"
}
}
EOF
# start as a background task
zot serve $TEST_TMPDIR/zot-config.json &
pid=$!
# wait until service is up
count=5
up=0
while [[ $count -gt 0 ]]; do
if [ ! -d /proc/$pid ]; then
echo "zot failed to start or died"
exit 1
fi
up=1
curl -f http://$ZOT_HOST:$ZOT_PORT/v2/ || up=0
if [ $up -eq 1 ]; then break; fi
sleep 1
count=$((count - 1))
done
if [ $up -eq 0 ]; then
echo "Timed out waiting for zot"
exit 1
fi
# setup a OCI client
regctl registry set --tls=disabled $ZOT_HOST:$ZOT_PORT
}

function zot_teardown {
killall zot
rm -f $TEST_TMPDIR/zot-config.json
}

0 comments on commit fdddd5b

Please sign in to comment.