Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

use cobra flags #38

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .github/workflows/build-snapshot.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ jobs:

- uses: actions/setup-go@3041bf56c941b39c61721a86cd11f3bb1338122a # v5.2.0
with:
go-version-file: './go.mod'
go-version: '1.23'
check-latest: true

- uses: sigstore/cosign-installer@dc72c7d5c4d10cd6bcb8cf6e3fd625a9e5e537da # v3.7.0
Expand All @@ -32,3 +32,7 @@ jobs:
args: release --clean --skip=sign --snapshot
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

- name: check binary
run: |
./dist/incert-linux-amd64 --help
2 changes: 1 addition & 1 deletion .github/workflows/release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ jobs:

- uses: actions/setup-go@3041bf56c941b39c61721a86cd11f3bb1338122a # v5.2.0
with:
go-version-file: './go.mod'
go-version: '1.23'
check-latest: true

- uses: goreleaser/goreleaser-action@9ed2f89a662bf1735a48bc8557fd212fa902bebf # v6
Expand Down
2 changes: 1 addition & 1 deletion .goreleaser.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ checksum:
name_template: "{{ .ProjectName }}_checksums.txt"

snapshot:
name_template: SNAPSHOT-{{ .ShortCommit }}
version_template: SNAPSHOT-{{ .ShortCommit }}

release:
prerelease: auto
Expand Down
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
Loading