Skip to content

Commit

Permalink
use cobra flags
Browse files Browse the repository at this point in the history
  • Loading branch information
cpanato committed Jan 21, 2025
1 parent 7fd1a2c commit c46438c
Show file tree
Hide file tree
Showing 4 changed files with 94 additions and 66 deletions.
39 changes: 18 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,34 +21,31 @@ Alternatively, you can also build from source by cloning the repo and running `g
`incert` supports the following flags:

```shell
-ca-certs-file string
The path to the local CA certificates file
-ca-certs-image-url string
The URL of an image to extract the CA certificates from
-dest-image-url string
The URL of the image to push the modified image to
-image-cert-path string
The path to the certificate file in the image (optional) (default "/etc/ssl/certs/ca-certificates.crt")
-image-url string
The URL of the image to append the CA certificates to
-output-certs-path string
Output the (appended) certificates file from the image to a local file (optional)
-owner-group-id int
The group ID of the owner of the certificate file in the image (optional)
-owner-user-id int
The user ID of the owner of the certificate file in the image (optional)
-platform string
The platform to build the image for (default "linux/amd64")
-replace-certs
Replace the certificates in the certificate file instead of appending them
Appends CA certificates to Docker images and pushes the modified image to a specified registry.

Usage:
incert [flags]

Flags:
--ca-certs-file string The path to the local CA certificates file
--ca-certs-image-url string The URL of an image to extract the CA certificates from
--dest-image-url string The URL of the image to push the modified image to
-h, --help help for incert
--image-cert-path string The path to the certificate file in the image (optional) (default "/etc/ssl/certs/ca-certificates.crt")
--image-url string The URL of the image to append the CA certificates to
--output-certs-path string Output the (appended) certificates file from the image to a local file (optional)
--owner-group-id int The group ID of the owner of the certificate file in the image (optional)
--owner-user-id int The user ID of the owner of the certificate file in the image (optional)
--platform string The platform to build the image for (default "linux/amd64")
--replace-certs Replace the certificates in the certificate file instead of appending them
```

## Example

To append a corporate CA certificate to an image, use the following command:

```bash
$ incert -image-url=mycompany/myimage:latest -ca-certs-file=/path/to/cacerts.pem -dest-image-url=myregistry/myimage:latest
$ incert --image-url=mycompany/myimage:latest --ca-certs-file=/path/to/cacerts.pem --dest-image-url=myregistry/myimage:latest
```

This will append the certificates in `/path/to/cacerts.pem` to the `mycompany/myimage:latest` image and push the modified image to `myregistry/myimage:latest`.
Expand Down
7 changes: 6 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,24 @@ go 1.23.0

toolchain go1.23.5

require github.com/google/go-containerregistry v0.20.3
require (
github.com/google/go-containerregistry v0.20.3
github.com/spf13/cobra v1.8.1
)

require (
github.com/containerd/stargz-snapshotter/estargz v0.16.3 // indirect
github.com/docker/cli v27.5.0+incompatible // indirect
github.com/docker/distribution v2.8.3+incompatible // indirect
github.com/docker/docker-credential-helpers v0.8.2 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/klauspost/compress v1.17.11 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.0 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/vbatts/tar-split v0.11.6 // indirect
golang.org/x/sync v0.10.0 // indirect
golang.org/x/sys v0.29.0 // indirect
Expand Down
8 changes: 8 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
github.com/containerd/stargz-snapshotter/estargz v0.16.3 h1:7evrXtoh1mSbGj/pfRccTampEyKpjpOnS3CyiV1Ebr8=
github.com/containerd/stargz-snapshotter/estargz v0.16.3/go.mod h1:uyr4BfYfOj3G9WBVE8cOlQmXAbPN9VEQpBBeJIuOipU=
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
Expand All @@ -13,6 +14,8 @@ github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-containerregistry v0.20.3 h1:oNx7IdTI936V8CQRveCjaxOiegWwvM7kqkbXTpyiovI=
github.com/google/go-containerregistry v0.20.3/go.mod h1:w00pIgBRDVUDFM6bq+Qx8lwNWK+cxgCuX1vd3PIBDNI=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
Expand All @@ -25,8 +28,13 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
Expand Down
106 changes: 62 additions & 44 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,15 @@ import (
"archive/tar"
"bytes"
"encoding/pem"
"flag"
"errors"
"fmt"
"io"
"log"
"os"
"strings"

"github.com/spf13/cobra"

"github.com/google/go-containerregistry/pkg/authn"
"github.com/google/go-containerregistry/pkg/name"
v1 "github.com/google/go-containerregistry/pkg/v1"
Expand All @@ -38,99 +40,113 @@ var (
)

func init() {
flag.StringVar(&imageURL, "image-url", "", "The URL of the image to append the CA certificates to")
flag.StringVar(&caCertFile, "ca-certs-file", "", "The path to the local CA certificates file")
flag.StringVar(&caCertsImageURL, "ca-certs-image-url", "", "The URL of an image to extract the CA certificates from")
flag.StringVar(&destImageURL, "dest-image-url", "", "The URL of the image to push the modified image to")
flag.StringVar(&platformStr, "platform", "linux/amd64", "The platform to build the image for")

flag.StringVar(&imageCertPath, "image-cert-path", "/etc/ssl/certs/ca-certificates.crt", "The path to the certificate file in the image (optional)")
flag.IntVar(&ownerUserID, "owner-user-id", 0, "The user ID of the owner of the certificate file in the image (optional)")
flag.IntVar(&ownerGroupID, "owner-group-id", 0, "The group ID of the owner of the certificate file in the image (optional)")
flag.StringVar(&outputCerts, "output-certs-path", "", "Output the (appended) certificates file from the image to a local file (optional)")
flag.BoolVar(&replaceCerts, "replace-certs", false, "Replace the certificates in the certificate file instead of appending them")
rootCmd.Flags().StringVar(&imageURL, "image-url", "", "The URL of the image to append the CA certificates to")
rootCmd.Flags().StringVar(&caCertFile, "ca-certs-file", "", "The path to the local CA certificates file")
rootCmd.Flags().StringVar(&caCertsImageURL, "ca-certs-image-url", "", "The URL of an image to extract the CA certificates from")
rootCmd.Flags().StringVar(&destImageURL, "dest-image-url", "", "The URL of the image to push the modified image to")
rootCmd.Flags().StringVar(&platformStr, "platform", "linux/amd64", "The platform to build the image for")

rootCmd.Flags().StringVar(&imageCertPath, "image-cert-path", "/etc/ssl/certs/ca-certificates.crt", "The path to the certificate file in the image (optional)")
rootCmd.Flags().IntVar(&ownerUserID, "owner-user-id", 0, "The user ID of the owner of the certificate file in the image (optional)")
rootCmd.Flags().IntVar(&ownerGroupID, "owner-group-id", 0, "The group ID of the owner of the certificate file in the image (optional)")
rootCmd.Flags().StringVar(&outputCerts, "output-certs-path", "", "Output the (appended) certificates file from the image to a local file (optional)")
rootCmd.Flags().BoolVar(&replaceCerts, "replace-certs", false, "Replace the certificates in the certificate file instead of appending them")

_ = rootCmd.MarkFlagRequired("image-url")
_ = rootCmd.MarkFlagRequired("dest-image-url")
}

func usage() {
flag.Usage()
os.Exit(1)
func main() {
if err := rootCmd.Execute(); err != nil {
os.Exit(1)
}
}

func main() {
var rootCmd = &cobra.Command{
Use: "incert",
Short: "Appends CA certificates to Docker images and pushes the modified image to a specified registry.",
SilenceUsage: true,
RunE: func(cmd *cobra.Command, args []string) error {
return do(cmd, args)
},
}

// Fetch the remote image
func fetchImage(imageURL string, platform v1.Platform) (v1.Image, error) {
ref, err := name.ParseReference(imageURL)
if err != nil {
return nil, err
}
img, err := remote.Image(ref, remote.WithAuthFromKeychain(authn.DefaultKeychain), remote.WithPlatform((platform)))
if err != nil {
return nil, err
}
return img, nil
}

func do(_ *cobra.Command, _ []string) error {
var platform v1.Platform
flag.Parse()

if imageURL == "" || destImageURL == "" || (caCertFile == "" && caCertsImageURL == "") {
usage()
if caCertFile == "" && caCertsImageURL == "" {
return errors.New("either --ca-certs-file or --ca-certs-image-url must be provided")
}

if platformStr != "" {
p, err := v1.ParsePlatform(platformStr)
if err != nil {
log.Fatalf("Failed to parse platform: %s", err)
return fmt.Errorf("Failed to parse platform: %s", err)
}
platform = *p
}

// Get the cert bytes
caCertBytes, err := getCertBytes(platform)
if err != nil {
log.Fatalf("Failed to get certificate bytes: %s", err)
return fmt.Errorf("Failed to get certificate bytes: %s", err)
}

// Sanity check to make sure the caCertBytes are actually a list of pem-encoded certificates
block, _ := pem.Decode(caCertBytes)
if block == nil || block.Type != "CERTIFICATE" {
log.Fatalf("Failed to find any certificates in %s", caCertFile)
return fmt.Errorf("Failed to find any certificates in %s", caCertFile)
}

img, err := fetchImage(imageURL, platform)
if err != nil {
log.Fatalf("Failed to fetch image %s: %s\n", imageURL, err)
return fmt.Errorf("Failed to fetch image %s: %s\n", imageURL, err)
}

newImg, err := newImage(img, caCertBytes)
if err != nil {
log.Fatalf("Failed to create new image: %s\n", err)
return fmt.Errorf("Failed to create new image: %s\n", err)
}

if outputCerts != "" {
if err := os.WriteFile(outputCerts, caCertBytes, 0644); err != nil {
log.Fatalf("Failed to write certificates to file %s: %s.\n", outputCerts, err)
return fmt.Errorf("Failed to write certificates to file %s: %s.\n", outputCerts, err)
}
}

newRef, err := name.ParseReference(destImageURL)
if err != nil {
log.Fatalf("Failed to parse destination image URL %s: %s\n", destImageURL, err)
return fmt.Errorf("Failed to parse destination image URL %s: %s\n", destImageURL, err)
}

// Push the modified image back to the registry
err = remote.Write(newRef, newImg, remote.WithAuthFromKeychain(authn.DefaultKeychain))
if err != nil {
log.Fatalf("Failed to push modified image %s: %s\n", newRef.String(), err)
return fmt.Errorf("Failed to push modified image %s: %s\n", newRef.String(), err)
}

fmt.Fprintf(os.Stderr, "Successfully appended CA certificates to image %s\n", newRef.String())
h, err := newImg.Digest()
if err != nil {
log.Fatalf("Failed to get digest of image %s: %s\n", newRef.String(), err)
return fmt.Errorf("Failed to get digest of image %s: %s\n", newRef.String(), err)
}

fmt.Printf("%s@sha256:%s\n", newRef.String(), h.Hex)
}

// Fetch the remote image
func fetchImage(imageURL string, platform v1.Platform) (v1.Image, error) {
ref, err := name.ParseReference(imageURL)
if err != nil {
return nil, err
}
img, err := remote.Image(ref, remote.WithAuthFromKeychain(authn.DefaultKeychain), remote.WithPlatform((platform)))
if err != nil {
return nil, err
}
return img, nil
return nil
}

func getCertBytes(platform v1.Platform) ([]byte, error) {
Expand All @@ -139,21 +155,22 @@ func getCertBytes(platform v1.Platform) ([]byte, error) {
// Read the contents of the local CA certificates file
caCertBytes, err := os.ReadFile(caCertFile)
if err != nil {
log.Fatalf("Failed to read CA certificates file %s: %s\n", caCertFile, err)
return []byte{}, fmt.Errorf("Failed to read CA certificates file %s: %s\n", caCertFile, err)
}

// Sanity check to make sure the caCertBytes are actually a list of pem-encoded certificates
block, _ := pem.Decode(caCertBytes)
if block == nil || block.Type != "CERTIFICATE" {
log.Fatalf("Failed to find any certificates in %s", caCertFile)
return []byte{}, fmt.Errorf("Failed to find any certificates in %s", caCertFile)
}
return caCertBytes, nil
} else {
// Fetch the remote image and its manifest
img, err := fetchImage(caCertsImageURL, platform)
if err != nil {
log.Fatalf("Failed to fetch image %s: %s\n", caCertsImageURL, err)
return []byte{}, fmt.Errorf("Failed to fetch image %s: %s\n", caCertsImageURL, err)
}

return extractCACerts(img)
}
}
Expand All @@ -173,6 +190,7 @@ func extractCACerts(img v1.Image) ([]byte, error) {
return io.ReadAll(tr)
}
}

return nil, fmt.Errorf("failed to find %s in remote image", imageCertPath)
}

Expand All @@ -191,7 +209,7 @@ func newImage(old v1.Image, caCertBytes []byte) (v1.Image, error) {
// Create a new tar file with the modified ca-certificates file
buf := bytes.Buffer{}
newTar := tar.NewWriter(&buf)
newTar.WriteHeader(&tar.Header{
_ = newTar.WriteHeader(&tar.Header{
Name: imageCertPath,
Mode: 0644,
Size: int64(len(newCaCertBytes)),
Expand Down

0 comments on commit c46438c

Please sign in to comment.