From c46438ceaec332a97618042f4d4a3422e110b450 Mon Sep 17 00:00:00 2001 From: cpanato Date: Tue, 21 Jan 2025 14:05:10 +0100 Subject: [PATCH] use cobra flags --- README.md | 39 ++++++++++---------- go.mod | 7 +++- go.sum | 8 +++++ main.go | 106 +++++++++++++++++++++++++++++++----------------------- 4 files changed, 94 insertions(+), 66 deletions(-) diff --git a/README.md b/README.md index 871b584..80fa1f8 100644 --- a/README.md +++ b/README.md @@ -21,26 +21,23 @@ 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 @@ -48,7 +45,7 @@ Alternatively, you can also build from source by cloning the repo and running `g 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`. diff --git a/go.mod b/go.mod index 6787e8a..9a3345d 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index 004eb77..035080a 100644 --- a/go.sum +++ b/go.sum @@ -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= @@ -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= @@ -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= diff --git a/main.go b/main.go index 7e83948..caff88e 100644 --- a/main.go +++ b/main.go @@ -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" @@ -38,37 +40,61 @@ 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 } @@ -76,61 +102,51 @@ func main() { // 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) { @@ -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) } } @@ -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) } @@ -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)),