diff --git a/.goreleaser.yml b/.goreleaser.yml index a91d666..baa02c0 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -47,6 +47,31 @@ builds: - GOARCH={{ .Arch }} post: - cmd: make SKIP_UPX={{ .IsSnapshot }} GOOS={{ .Os }} GOARCH={{ .Arch }} UPX_TARGET={{ .Path }} upx + - id: static-credential-provider + dir: ./cmd/static-credential-provider + binary: static-credential-provider + env: + - CGO_ENABLED=0 + flags: + - -trimpath + ldflags: + - -s + - -w + goos: + - linux + - darwin + goarch: + - amd64 + - arm64 + mod_timestamp: '{{ .CommitTimestamp }}' + hooks: + pre: + - cmd: make SKIP_UPX={{ .IsSnapshot }} go-generate + env: + - GOOS={{ .Os }} + - GOARCH={{ .Arch }} + post: + - cmd: make SKIP_UPX={{ .IsSnapshot }} GOOS={{ .Os }} GOARCH={{ .Arch }} UPX_TARGET={{ .Path }} upx archives: - name_template: '{{ .ProjectName }}_v{{trimprefix .Version "v"}}_{{ .Os }}_{{ .Arch }}' # This is a hack documented in https://github.com/goreleaser/goreleaser/blob/df0216d5855e9283d2106fb5acdb0e7b528a56e8/www/docs/customization/archive.md#packaging-only-the-binaries @@ -54,6 +79,13 @@ archives: - none* builds: - kubelet-image-credential-provider-shim + - name_template: 'static-credential-provider_v{{trimprefix .Version "v"}}_{{ .Os }}_{{ .Arch }}' + # This is a hack documented in https://github.com/goreleaser/goreleaser/blob/df0216d5855e9283d2106fb5acdb0e7b528a56e8/www/docs/customization/archive.md#packaging-only-the-binaries + id: static-credential-provider + files: + - none* + builds: + - static-credential-provider dockers: - image_templates: # Specify the image tag including `-amd64` suffix if the build is not a snapshot build or is not being built on diff --git a/Dockerfile.credential-providers-installer b/Dockerfile.credential-providers-installer index 974080a..ec99f36 100644 --- a/Dockerfile.credential-providers-installer +++ b/Dockerfile.credential-providers-installer @@ -25,6 +25,8 @@ RUN --mount=type=bind,src=credential-providers,target=/go/src/credential-provide -o /go/bin/acr-credential-provider \ sigs.k8s.io/cloud-provider-azure/cmd/acr-credential-provider +COPY static-credential-provider /go/bin/static-credential-provider + # Use distroless/static:nonroot image for a base. FROM --platform=linux/amd64 gcr.io/distroless/static@sha256:6e5f8857479b83d032a14a17f8e0731634c6b8b5e225f53a039085ec1f7698c6 as linux-amd64 FROM --platform=linux/arm64 gcr.io/distroless/static@sha256:d79a4342bd72644f30436ae22e55ab68a7c3a125e91d76936bcb2be66aa2af57 as linux-arm64 @@ -37,7 +39,7 @@ USER 65532 COPY kubelet-image-credential-provider-shim /usr/local/bin/kubelet-image-credential-provider-shim COPY --from=credential_provider_builder \ - /go/bin/ecr-credential-provider /go/bin/acr-credential-provider \ + /go/bin/ecr-credential-provider /go/bin/acr-credential-provider /go/bin/static-credential-provider \ /opt/image-credential-provider/bin/ ENTRYPOINT ["/usr/local/bin/kubelet-image-credential-provider-shim", "install"] diff --git a/cmd/kubelet-image-credential-provider-shim/main.go b/cmd/kubelet-image-credential-provider-shim/main.go index a28d1e2..9dcbfb9 100644 --- a/cmd/kubelet-image-credential-provider-shim/main.go +++ b/cmd/kubelet-image-credential-provider-shim/main.go @@ -22,6 +22,6 @@ func main() { rootCmd.AddCommand(newInstallCmd(logger)) if err := rootCmd.Execute(); err != nil { - os.Exit(1) + logger.Fatal(err) } } diff --git a/cmd/static-credential-provider/main.go b/cmd/static-credential-provider/main.go new file mode 100644 index 0000000..e4be965 --- /dev/null +++ b/cmd/static-credential-provider/main.go @@ -0,0 +1,50 @@ +// Copyright 2022 D2iQ, Inc. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "context" + "fmt" + + "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + + "github.com/mesosphere/kubelet-image-credential-provider-shim/pkg/credentialprovider" +) + +const ( + //nolint:gosec // not credentials + defaultCredentialsFile = "/etc/kubernetes/image-credentials.json" +) + +func main() { + logger := logrus.New() + + rootCmd := &cobra.Command{ + Use: "static-credential-provider ", + Short: "Static credential provider for Kubelet", + Args: cobra.MaximumNArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + credentialsFile := defaultCredentialsFile + if len(args) == 1 { + credentialsFile = args[0] + } + provider, err := credentialprovider.NewStaticProvider(credentialsFile) + if err != nil { + return fmt.Errorf("error initializing static credential provider: %w", err) + } + + err = credentialprovider.NewCredentialProvider(provider).Run(context.TODO()) + if err != nil { + return fmt.Errorf("error running static credential provider: %w", err) + } + + return nil + }, + } + + if err := rootCmd.Execute(); err != nil { + logger.Fatal(err) + } +} diff --git a/go.mod b/go.mod index 83adb7e..dad226e 100644 --- a/go.mod +++ b/go.mod @@ -12,17 +12,33 @@ require ( github.com/spf13/cobra v1.6.1 github.com/stretchr/testify v1.8.1 go.etcd.io/etcd/client/pkg/v3 v3.5.5 + k8s.io/apimachinery v0.25.3 + k8s.io/kubelet v0.25.3 ) require ( github.com/davecgh/go-spew v1.1.1 // indirect + github.com/go-logr/logr v1.2.3 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/google/gofuzz v1.1.0 // indirect github.com/inconshreveable/mousetrap v1.0.1 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/spf13/pflag v1.0.5 // indirect go.uber.org/atomic v1.7.0 // indirect go.uber.org/multierr v1.6.0 // indirect go.uber.org/zap v1.17.0 // indirect + golang.org/x/net v0.0.0-20220722155237-a158d28d115b // indirect golang.org/x/sys v0.0.0-20220913175220-63ea55921009 // indirect + golang.org/x/text v0.3.7 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect + k8s.io/klog/v2 v2.70.1 // indirect + k8s.io/utils v0.0.0-20220728103510-ee6ede2d64ed // indirect + sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2 // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect + sigs.k8s.io/yaml v1.2.0 // indirect ) diff --git a/go.sum b/go.sum index ec5fd4e..14635f6 100644 --- a/go.sum +++ b/go.sum @@ -3,11 +3,32 @@ github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46t 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= +github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= +github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0= +github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.1.0 h1:Hsa8mG0dQ46ij8Sl2AYJDUv1oA9/d6Vk+3LG99Oe02g= +github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc= github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8= github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= github.com/otiai10/copy v1.9.0 h1:7KFNiCgZ91Ru4qW4CWPf/7jqtxLagGRmIxWldPP9VY4= github.com/otiai10/copy v1.9.0/go.mod h1:hsfX19wcn0UWIHUQ3/4fHuehhk2UyArQ9dVFAn3FczI= github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE= @@ -15,13 +36,14 @@ github.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6 github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo= github.com/otiai10/mint v1.4.0 h1:umwcf7gbpEwf7WFzqmWwSv0CzbeMsae2u9ZvpP8j2q4= github.com/otiai10/mint v1.4.0/go.mod h1:gifjb2MYOoULtKLqUAEILUG/9KONW6f7YsJ6vQLTlFI= -github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 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.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= github.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA= github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= @@ -35,6 +57,8 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= go.etcd.io/etcd/client/pkg/v3 v3.5.5 h1:9S0JUVvmrVl7wCF39iTQthdaaNIiAaQbmK75ogO6GU8= go.etcd.io/etcd/client/pkg/v3 v3.5.5/go.mod h1:ggrwbk069qxpKPq8/FKkQ3Xq9y39kbFR4LnKszpRXeQ= go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= @@ -43,12 +67,43 @@ go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= go.uber.org/zap v1.17.0 h1:MTjgFu6ZLKvY6Pvaqk97GlxNBuMpV4Hy/3P6tRGlI2U= go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b h1:PxfKdU9lEEDYjdIzOtC4qFWgkU2rGHdKlKowJSMN9h0= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220913175220-63ea55921009 h1:PuvuRMeLWqsf/ZdT1UUZz0syhioyv1mzuFZsXs4fvhw= golang.org/x/sys v0.0.0-20220913175220-63ea55921009/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= @@ -56,3 +111,18 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +k8s.io/apimachinery v0.25.3 h1:7o9ium4uyUOM76t6aunP0nZuex7gDf8VGwkR5RcJnQc= +k8s.io/apimachinery v0.25.3/go.mod h1:jaF9C/iPNM1FuLl7Zuy5b9v+n35HGSh6AQ4HYRkCqwo= +k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE= +k8s.io/klog/v2 v2.70.1 h1:7aaoSdahviPmR+XkS7FyxlkkXs6tHISSG03RxleQAVQ= +k8s.io/klog/v2 v2.70.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= +k8s.io/kubelet v0.25.3 h1:PjT3Xo0VL1BpRilBpZrRN8pSy6w5pGQ0YDQQeQWSHvQ= +k8s.io/kubelet v0.25.3/go.mod h1:YopVc6vLhveZb22I7AzcoWPap+t3/KJKqRZDa2MZmyE= +k8s.io/utils v0.0.0-20220728103510-ee6ede2d64ed h1:jAne/RjBTyawwAy0utX5eqigAwz/lQhTmy+Hr/Cpue4= +k8s.io/utils v0.0.0-20220728103510-ee6ede2d64ed/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= +sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2 h1:iXTIw73aPyC+oRdyqqvVJuloN1p0AC/kzH07hu3NE+k= +sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= +sigs.k8s.io/structured-merge-diff/v4 v4.2.3 h1:PRbqxJClWWYMNV1dhaG4NsibJbArud9kFxnAMREiWFE= +sigs.k8s.io/structured-merge-diff/v4 v4.2.3/go.mod h1:qjx8mGObPmV2aSZepjQjbmb2ihdVs8cGKBraizNC69E= +sigs.k8s.io/yaml v1.2.0 h1:kr/MCeFWJWTwyaHoR9c8EjH9OumOmoF9YGiZd7lFm/Q= +sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= diff --git a/pkg/credentialprovider/plugin.go b/pkg/credentialprovider/plugin.go new file mode 100644 index 0000000..7919cd9 --- /dev/null +++ b/pkg/credentialprovider/plugin.go @@ -0,0 +1,170 @@ +// Copyright 2022 D2iQ, Inc. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package credentialprovider + +import ( + "bufio" + "context" + "errors" + "fmt" + "io" + "os" + + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/serializer" + "k8s.io/apimachinery/pkg/runtime/serializer/json" + "k8s.io/kubelet/pkg/apis/credentialprovider/install" + "k8s.io/kubelet/pkg/apis/credentialprovider/v1alpha1" +) + +var ( + scheme = runtime.NewScheme() + codecs = serializer.NewCodecFactory(scheme) +) + +//nolint:gochecknoinits // init is idiomatically used to set up schemes +func init() { + install.Install(scheme) +} + +// CredentialProvider is an interface implemented by the kubelet credential provider plugin to fetch +// the username/password based on the provided image name. +type CredentialProvider interface { + GetCredentials( + ctx context.Context, + image string, + args []string, + ) (response *v1alpha1.CredentialProviderResponse, err error) +} + +// ExecPlugin implements the exec-based plugin for fetching credentials that is invoked by the kubelet. +type ExecPlugin struct { + plugin CredentialProvider +} + +// NewCredentialProvider returns an instance of execPlugin that fetches +// credentials based on the provided plugin implementing the CredentialProvider interface. +func NewCredentialProvider(plugin CredentialProvider) *ExecPlugin { + return &ExecPlugin{plugin} +} + +// Run executes the credential provider plugin. Required information for the plugin request (in +// the form of v1alpha1.CredentialProviderRequest) is provided via stdin from the kubelet. +// The CredentialProviderResponse, containing the username/password required for pulling +// the provided image, will be sent back to the kubelet via stdout. +func (e *ExecPlugin) Run(ctx context.Context) error { + return e.runPlugin(ctx, os.Stdin, os.Stdout, os.Args[1:]) +} + +var ( + ErrEmptyImageInRequest = errors.New("image in plugin request was empty") + ErrNilCredentialProviderResponse = errors.New("CredentialProviderResponse from plugin was nil") + ErrUnsupportedAPIVersion = errors.New("unsupported API version") +) + +func (e *ExecPlugin) runPlugin(ctx context.Context, r io.Reader, w io.Writer, args []string) error { + data, err := io.ReadAll(r) + if err != nil { + return err + } + + gvk, err := json.DefaultMetaFactory.Interpret(data) + if err != nil { + return err + } + + if gvk.GroupVersion() != v1alpha1.SchemeGroupVersion { + return fmt.Errorf("%w: %s", ErrUnsupportedAPIVersion, gvk) + } + + request, err := decodeRequest(data) + if err != nil { + return err + } + + if request.Image == "" { + return fmt.Errorf("%w", ErrEmptyImageInRequest) + } + + response, err := e.plugin.GetCredentials(ctx, request.Image, args) + if err != nil { + return err + } + + if response == nil { + return fmt.Errorf("%w", ErrNilCredentialProviderResponse) + } + + encodedResponse, err := encodeResponse(response) + if err != nil { + return err + } + + writer := bufio.NewWriter(w) + defer writer.Flush() + if _, err := writer.Write(encodedResponse); err != nil { + return err + } + + return nil +} + +var ( + ErrUnsupportedRequestKind = errors.New( + "unsupported credential provider request kind", + ) + ErrConversionFailure = errors.New("conversion failure") +) + +func decodeRequest(data []byte) (*v1alpha1.CredentialProviderRequest, error) { + obj, gvk, err := codecs.UniversalDecoder(v1alpha1.SchemeGroupVersion).Decode(data, nil, nil) + if err != nil { + if runtime.IsNotRegisteredError(err) { + return nil, fmt.Errorf("%w: %v", ErrUnsupportedRequestKind, err) + } + return nil, err + } + + if gvk.Kind != "CredentialProviderRequest" { + return nil, fmt.Errorf( + "%w: %s (expected CredentialProviderRequest)", + ErrUnsupportedRequestKind, + gvk.Kind, + ) + } + + if gvk.Group != v1alpha1.GroupName { + return nil, fmt.Errorf( + "%w: %s (expected %s)", + ErrUnsupportedAPIVersion, gvk.GroupVersion(), v1alpha1.SchemeGroupVersion, + ) + } + + request, ok := obj.(*v1alpha1.CredentialProviderRequest) + if !ok { + return nil, fmt.Errorf( + "%w: unable to convert %T to *CredentialProviderRequest", + ErrConversionFailure, + obj, + ) + } + + return request, nil +} + +func encodeResponse(response *v1alpha1.CredentialProviderResponse) ([]byte, error) { + mediaType := "application/json" + info, ok := runtime.SerializerInfoForMediaType(codecs.SupportedMediaTypes(), mediaType) + if !ok { + return nil, fmt.Errorf("unsupported media type %q", mediaType) + } + + encoder := codecs.EncoderForVersion(info.Serializer, v1alpha1.SchemeGroupVersion) + data, err := runtime.Encode(encoder, response) + if err != nil { + return nil, fmt.Errorf("failed to encode response: %v", err) + } + + return data, nil +} diff --git a/pkg/credentialprovider/plugin_test.go b/pkg/credentialprovider/plugin_test.go new file mode 100644 index 0000000..d406b57 --- /dev/null +++ b/pkg/credentialprovider/plugin_test.go @@ -0,0 +1,90 @@ +// Copyright 2022 D2iQ, Inc. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package credentialprovider + +import ( + "bytes" + "context" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/kubelet/pkg/apis/credentialprovider/v1alpha1" +) + +type fakePlugin struct{} + +func (f *fakePlugin) GetCredentials( + ctx context.Context, + image string, + args []string, +) (*v1alpha1.CredentialProviderResponse, error) { + return &v1alpha1.CredentialProviderResponse{ + CacheKeyType: v1alpha1.RegistryPluginCacheKeyType, + CacheDuration: &metav1.Duration{Duration: 10 * time.Minute}, + Auth: map[string]v1alpha1.AuthConfig{ + "*.registry.io": { + Username: "user", + Password: "password", + }, + }, + }, nil +} + +func Test_runPlugin(t *testing.T) { + t.Parallel() + + testcases := []struct { + name string + req string + expectedOut string + expectErr error + }{ + { + name: "successful test case", + //nolint:lll // just a long string + req: `{"kind":"CredentialProviderRequest","apiVersion":"credentialprovider.kubelet.k8s.io/v1alpha1","image":"test.registry.io/foobar"}`, + //nolint:lll // just a long string + expectedOut: `{"kind":"CredentialProviderResponse","apiVersion":"credentialprovider.kubelet.k8s.io/v1alpha1","cacheKeyType":"Registry","cacheDuration":"10m0s","auth":{"*.registry.io":{"username":"user","password":"password"}}} +`, + }, + { + name: "invalid kind", + //nolint:lll // just a long string + req: `{"kind":"CredentialProviderFoo","apiVersion":"credentialprovider.kubelet.k8s.io/v1alpha1","image":"test.registry.io/foobar"}`, + expectErr: ErrUnsupportedRequestKind, + }, + { + name: "invalid apiVersion", + //nolint:lll // just a long string + req: `{"kind":"CredentialProviderRequest","apiVersion":"foo.k8s.io/v1alpha1","image":"test.registry.io/foobar"}`, + expectErr: ErrUnsupportedAPIVersion, + }, + { + name: "empty image", + //nolint:lll // just a long string + req: `{"kind":"CredentialProviderRequest","apiVersion":"credentialprovider.kubelet.k8s.io/v1alpha1","image":""}`, + expectErr: ErrEmptyImageInRequest, + }, + } + + for _, tt := range testcases { + tt := tt // Capture range variable. + + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + p := NewCredentialProvider(&fakePlugin{}) + + out := &bytes.Buffer{} + require.ErrorIs( + t, + p.runPlugin(context.TODO(), bytes.NewBufferString(tt.req), out, nil), + tt.expectErr, + ) + assert.Equal(t, tt.expectedOut, out.String()) + }) + } +} diff --git a/pkg/credentialprovider/static_credentials.go b/pkg/credentialprovider/static_credentials.go new file mode 100644 index 0000000..2f64e96 --- /dev/null +++ b/pkg/credentialprovider/static_credentials.go @@ -0,0 +1,58 @@ +// Copyright 2022 D2iQ, Inc. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package credentialprovider + +import ( + "context" + "fmt" + "os" + + "k8s.io/kubelet/pkg/apis/credentialprovider/v1alpha1" +) + +// staticProvider implements the credential provider interface, reading a credentials file on the disk. +type staticProvider struct { + credentialsFile string +} + +// NewStaticProvider creates a new instance of the static credentials provider. +func NewStaticProvider(credentialsFile string) (CredentialProvider, error) { + return staticProvider{credentialsFile: credentialsFile}, nil +} + +// GetCredentials will ignore the image and args arguments and simply read a credentials file and return its content. +func (s staticProvider) GetCredentials( + ctx context.Context, + image string, + args []string, +) (response *v1alpha1.CredentialProviderResponse, err error) { + credentials, err := os.ReadFile(s.credentialsFile) + if err != nil { + return nil, fmt.Errorf("error reading credentials file: %w", err) + } + + return decodeResponse(credentials) +} + +func decodeResponse(data []byte) (*v1alpha1.CredentialProviderResponse, error) { + obj, gvk, err := codecs.UniversalDecoder(v1alpha1.SchemeGroupVersion).Decode(data, nil, nil) + if err != nil { + return nil, err + } + + if gvk.Kind != "CredentialProviderResponse" { + return nil, fmt.Errorf("kind was %q, expected CredentialProviderResponse", gvk.Kind) + } + + if gvk.Group != v1alpha1.GroupName { + return nil, fmt.Errorf("group was %q, expected %s", gvk.Group, v1alpha1.GroupName) + } + + response, ok := obj.(*v1alpha1.CredentialProviderResponse) + if !ok { + return nil, fmt.Errorf("unable to convert %T to *CredentialProviderResponse", obj) + } + + return response, nil +} diff --git a/pkg/credentialprovider/static_credentials_test.go b/pkg/credentialprovider/static_credentials_test.go new file mode 100644 index 0000000..177e982 --- /dev/null +++ b/pkg/credentialprovider/static_credentials_test.go @@ -0,0 +1,116 @@ +// Copyright 2022 D2iQ, Inc. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package credentialprovider_test + +import ( + "bytes" + "context" + "os" + "path" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/kubelet/pkg/apis/credentialprovider/v1alpha1" + + "github.com/mesosphere/kubelet-image-credential-provider-shim/pkg/credentialprovider" +) + +var ( + //nolint:lll // just a long string + validCredentialsBytes = []byte( + `{"kind":"CredentialProviderResponse","apiVersion":"credentialprovider.kubelet.k8s.io/v1alpha1","cacheKeyType":"Registry","cacheDuration":"10m0s","auth":{"*.registry.io":{"username":"user","password":"password"}}}`, + ) + validCredentials = generateResponse("*.registry.io", 10*time.Minute, "user", "password") +) + +func TestGetCredentials(t *testing.T) { + tests := []struct { + name string + in *bytes.Buffer + credentialsBytes []byte + expectedOut *v1alpha1.CredentialProviderResponse + expectErr bool + }{ + { + name: "successful test case", + //nolint:lll // just a long string + in: bytes.NewBufferString( + `{"kind":"CredentialProviderRequest","apiVersion":"credentialprovider.kubelet.k8s.io/v1alpha1","image":"test.registry.io/foobar"}`, + ), + credentialsBytes: validCredentialsBytes, + expectedOut: validCredentials, + expectErr: false, + }, + { + name: "invalid kind", + //nolint:lll // just a long string + in: bytes.NewBufferString( + `{"kind":"CredentialProviderFoo","apiVersion":"credentialprovider.kubelet.k8s.io/v1alpha1","image":"test.registry.io/foobar"}`, + ), + expectedOut: nil, + expectErr: true, + }, + { + name: "invalid apiVersion", + in: bytes.NewBufferString( + `{"kind":"CredentialProviderRequest","apiVersion":"foo.k8s.io/v1alpha1","image":"test.registry.io/foobar"}`, + ), + expectedOut: nil, + expectErr: true, + }, + { + name: "empty image", + in: bytes.NewBufferString( + `{"kind":"CredentialProviderRequest","apiVersion":"credentialprovider.kubelet.k8s.io/v1alpha1","image":""}`, + ), + credentialsBytes: validCredentialsBytes, + expectedOut: validCredentials, + expectErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + credentialsFile := path.Join(t.TempDir(), "image-credentials.json") + err := os.WriteFile(credentialsFile, tt.credentialsBytes, 0o600) + require.NoError(t, err, "error writing temporary credentials file") + + provider, err := credentialprovider.NewStaticProvider(credentialsFile) + require.NoError(t, err, "error initializing static credential provider") + + resp, err := provider.GetCredentials(context.TODO(), "", nil) + + if err == nil && tt.expectErr { + t.Error("expected error but got none") + } + + assert.Equal(t, tt.expectedOut, resp) + }) + } +} + +func generateResponse( + registry string, + duration time.Duration, + username string, + password string, +) *v1alpha1.CredentialProviderResponse { + return &v1alpha1.CredentialProviderResponse{ + TypeMeta: metav1.TypeMeta{ + Kind: "CredentialProviderResponse", + APIVersion: "credentialprovider.kubelet.k8s.io/v1alpha1", + }, + CacheKeyType: v1alpha1.RegistryPluginCacheKeyType, + CacheDuration: &metav1.Duration{Duration: duration}, + Auth: map[string]v1alpha1.AuthConfig{ + registry: { + Username: username, + Password: password, + }, + }, + } +}