Skip to content

Commit

Permalink
examples/go/container_base_image: add example (#181)
Browse files Browse the repository at this point in the history
Add an example for the new QueryContainerImages API.
  • Loading branch information
josieang authored Jan 29, 2025
1 parent 9148c73 commit 7b38e5d
Show file tree
Hide file tree
Showing 4 changed files with 211 additions and 0 deletions.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ Example applications written in Go can be found in the `examples` directory:

- [`artifact_query`](examples/go/artifact_query) shows how to query the
deps.dev HTTP API by file content hash.
- [`container_base_image`](examples/go/container_base_image) shows how to
compute the Chain ID from an OCI container image and use the deps.dev HTTP
API to identify base image(s).
- [`dependencies_dot`](examples/go/dependencies_dot) fetches a resolved
dependency graph from the deps.dev HTTP API and renders it in the DOT
language used by Graphviz.
Expand Down
8 changes: 8 additions & 0 deletions examples/go/container_base_image/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
module github.com/google/deps.dev/examples/go/container_base_image

go 1.23.3

require (
github.com/opencontainers/go-digest v1.0.0
github.com/opencontainers/image-spec v1.1.0
)
4 changes: 4 additions & 0 deletions examples/go/container_base_image/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
196 changes: 196 additions & 0 deletions examples/go/container_base_image/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
// Copyright 2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

/*
container_base_image is a simple example application that queries the deps.dev
API for the base image(s) of an Open Container Image (OCI) compliant tarball.
The OCI spec is defined at
https://github.com/opencontainers/image-spec/blob/main/spec.md.
To produce an OCI-compliant tarball using the docker command, run `docker image
save <image id>` with a docker client v25.0 or above.
*/
package main

import (
"archive/tar"
"encoding/json"
"errors"
"flag"
"fmt"
"io"
"log"
"net/http"
"os"
"strings"

"github.com/opencontainers/go-digest"
"github.com/opencontainers/image-spec/identity"
)

// response is used to unmarshal QueryContainerImage responseses.
type response struct {
Results []struct {
Repository string
}
}

// https://github.com/opencontainers/image-spec/blob/main/image-layout.md#oci-layout-file
type ociLayout struct {
ImageLayoutVersion string `json:"imageLayoutVersion"`
}

// https://github.com/opencontainers/image-spec/blob/main/image-index.md
type index struct {
Manifests []struct {
Digest string `json:"digest"`
} `json:"manifests"`
}

// https://github.com/opencontainers/image-spec/blob/main/manifest.md
type manifest struct {
Config struct {
Digest string `json:"digest"`
} `json:"config"`
}

// https://github.com/opencontainers/image-spec/blob/main/config.md
type config struct {
RootFS struct {
DiffIDs []string `json:"diff_ids"`
} `json:"rootfs"`
}

func main() {
log.SetFlags(0)
flag.Usage = func() {
fmt.Fprintf(os.Stderr, "Usage: container_base_image <image.tar>\n")
flag.PrintDefaults()
}

flag.Parse()
if flag.NArg() != 1 {
flag.Usage()
os.Exit(1)
}
tarArchive := flag.Arg(0)
// Check that the tar declares itself to to be OCI 1.0.0 compliant.
var layout ociLayout
if err := unmarshalFile(tarArchive, "oci-layout", &layout); err != nil {
log.Fatalf("%v\nAre you using a docker client version >=25.0 to save the image?", err)
}
if layout.ImageLayoutVersion != "1.0.0" {
log.Fatalf("The oci-layout file lists version %v which is not supported.", layout.ImageLayoutVersion)
}

// Find the manifest(s) for the image. There may be multiple.
var idx index
if err := unmarshalFile(tarArchive, "index.json", &idx); err != nil {
log.Fatalf("%v", err)
}
fmt.Printf("%d manifest(s) found\n", len(idx.Manifests))

// For each manifest, look up the base image(s).
for _, m := range idx.Manifests {
// Read the manifest file, which contains the config digest.
var mt manifest
id, _ := strings.CutPrefix(m.Digest, "sha256:")
if err := unmarshalFile(tarArchive, fmt.Sprintf("blobs/sha256/%s", id), &mt); err != nil {
log.Fatalf("%v", err)
}
// Read the config file, which contains the diff IDs.
var c config
id, _ = strings.CutPrefix(mt.Config.Digest, "sha256:")
if err := unmarshalFile(tarArchive, fmt.Sprintf("blobs/sha256/%s", id), &c); err != nil {
log.Fatalf("%v", err)
}
// For each chain ID, query the deps.dev api to determine
// whether it's a known base image.
chainIDs := makeChainIDs(c.RootFS.DiffIDs)
for i, chainID := range chainIDs {
url := "https://api.deps.dev/v3alpha/querycontainerimages/" + chainID
resp, err := http.Get(url)
if err != nil {
log.Fatalf("Request: %v", err)
}
switch resp.StatusCode {
case http.StatusNotFound:
fmt.Printf("Layer %d: no base image found\n", i)
case http.StatusOK:
var respBody response
err = json.NewDecoder(resp.Body).Decode(&respBody)
if err != nil {
log.Fatalf("Decoding response body: %v", err)
}
var repos []string
for _, r := range respBody.Results {
repos = append(repos, r.Repository)
}
fmt.Printf("Layer %d: %v\n", i, strings.Join(repos, " "))
default:
log.Fatalf("Response: %v", resp.Status)
}
resp.Body.Close()
}
}
}

// unmarshalFile looks for the file with the specified filename in the specified
// tarArchive.
func unmarshalFile(tarArchive string, filename string, v any) error {
f, err := os.Open(tarArchive)
if err != nil {
return err
}
tr := tar.NewReader(f)
var b []byte
for {
hdr, err := tr.Next()
if errors.Is(err, io.EOF) {
break
}
if hdr.Name == filename {
b, err = io.ReadAll(io.Reader(tr))
if err != nil {
return err
}
break
}
}
if len(b) == 0 {
return fmt.Errorf("No %s in tar archive", filename)
}

return json.Unmarshal(b, v)
}

// makeChainIDs computes the chain ID for each prefix of layers. An OCI chain
// ID refers to a sequence of layers with a single identifier.
// https://github.com/opencontainers/image-spec/blob/main/config.md#layer-chainid
func makeChainIDs(diffIDs []string) []string {
if len(diffIDs) == 0 {
return nil
}
diffDigests := make([]digest.Digest, len(diffIDs))
for i, diffID := range diffIDs {
diffDigests[i] = digest.Digest(diffID)
}
chainDigests := identity.ChainIDs(diffDigests)
chainIDs := make([]string, len(chainDigests))
for i, chainDigest := range chainDigests {
chainIDs[i] = string(chainDigest)
}
return chainIDs
}

0 comments on commit 7b38e5d

Please sign in to comment.