Skip to content

Commit

Permalink
Merge pull request #2579 from replicatedhq/divolgin/copy-images
Browse files Browse the repository at this point in the history
CLI to copy Admin Console images to private registry
  • Loading branch information
divolgin committed Mar 3, 2022
2 parents 2bf1f93 + cce598f commit c9a7d70
Show file tree
Hide file tree
Showing 5 changed files with 230 additions and 42 deletions.
67 changes: 63 additions & 4 deletions cmd/kots/cli/admin-console-push-images.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
package cli

import (
"context"
"os"

"github.com/pkg/errors"
"github.com/replicatedhq/kots/pkg/docker/registry"
"github.com/replicatedhq/kots/pkg/k8sutil"
"github.com/replicatedhq/kots/pkg/kotsadm"
kotsadmtypes "github.com/replicatedhq/kots/pkg/kotsadm/types"
"github.com/spf13/cobra"
"github.com/spf13/viper"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

func AdminPushImagesCmd() *cobra.Command {
Expand All @@ -29,11 +32,23 @@ func AdminPushImagesCmd() *cobra.Command {
os.Exit(1)
}

airgapArchive := args[0]
imageSource := args[0]
endpoint := args[1]

namespace := v.GetString("namespace")
if namespace == "" {
namespace = metav1.NamespaceDefault
}

username := v.GetString("registry-username")
password := v.GetString("registry-password")
if username == "" && password == "" {
u, p, err := getRegistryCredentialsFromSecret(endpoint, v.GetString("namespace"))
if err != nil {
return errors.Wrap(err, "failed get registry login from secret")
}
username, password = u, p
}

if registry.IsECREndpoint(endpoint) && username != "AWS" {
var err error
Expand All @@ -55,9 +70,19 @@ func AdminPushImagesCmd() *cobra.Command {
ProgressWriter: os.Stdout,
}

err := kotsadm.PushImages(airgapArchive, options)
if err != nil {
return errors.Wrap(err, "failed to push images")
_, err := os.Stat(imageSource)
if err == nil {
err := kotsadm.PushImages(imageSource, options)
if err != nil {
return errors.Wrap(err, "failed to push images")
}
} else if os.IsNotExist(err) {
err := kotsadm.CopyImages(imageSource, options, namespace)
if err != nil {
return errors.Wrap(err, "failed to push images")
}
} else {
return errors.Wrap(err, "failed to stat file")
}

return nil
Expand All @@ -72,3 +97,37 @@ func AdminPushImagesCmd() *cobra.Command {

return cmd
}

func getRegistryCredentialsFromSecret(endpoint string, namespace string) (username string, password string, err error) {
if namespace == "" {
namespace = metav1.NamespaceDefault
}

clientset, err := k8sutil.GetClientset()
if err != nil {
err = errors.Wrap(err, "failed to get clientset")
return
}

secret, err := clientset.CoreV1().Secrets(namespace).Get(context.TODO(), "kotsadm-replicated-registry", metav1.GetOptions{})
if err != nil {
err = errors.Wrap(err, "failed to get secret")
return
}

dockerConfigJson := secret.Data[".dockerconfigjson"]
if len(dockerConfigJson) == 0 {
err = errors.New("no .dockerconfigjson found in secret")
return
}

credentials, err := registry.GetCredentialsForRegistryFromConfigJSON(dockerConfigJson, endpoint)
if err != nil {
err = errors.Wrap(err, "failed to get credentials")
return
}

username = credentials.Username
password = credentials.Password
return
}
45 changes: 22 additions & 23 deletions pkg/image/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -230,15 +230,6 @@ func listImagesInFile(contents []byte, handler processImagesFunc) error {
}

func processOneImage(srcRegistry, destRegistry registry.RegistryOptions, image string, appSlug string, reportWriter io.Writer, log *logger.CLILogger, copyImages, allImagesPrivate bool, checkedImages map[string]ImageInfo, dockerHubRegistry registry.RegistryOptions) ([]kustomizeimage.Image, error) {
policy, err := signature.NewPolicyFromBytes(imagePolicy)
if err != nil {
return nil, errors.Wrap(err, "failed to read default policy")
}
policyContext, err := signature.NewPolicyContext(policy)
if err != nil {
return nil, errors.Wrap(err, "failed to create policy")
}

sourceCtx := &types.SystemContext{DockerDisableV1Ping: true}

// allow pulling images from http/invalid https docker repos
Expand Down Expand Up @@ -316,7 +307,7 @@ func processOneImage(srcRegistry, destRegistry registry.RegistryOptions, image s
return kustomizeImage(destRegistry, image)
}

_, err = CopyImageWithGC(context.Background(), policyContext, destRef, srcRef, &copy.Options{
_, err = CopyImageWithGC(context.Background(), destRef, srcRef, &copy.Options{
RemoveSignatures: true,
SignBy: "",
ReportWriter: reportWriter,
Expand Down Expand Up @@ -345,7 +336,7 @@ func processOneImage(srcRegistry, destRegistry registry.RegistryOptions, image s
}

// copy image from remote to local
_, err = CopyImageWithGC(context.Background(), policyContext, localRef, srcRef, &copy.Options{
_, err = CopyImageWithGC(context.Background(), localRef, srcRef, &copy.Options{
RemoveSignatures: true,
SignBy: "",
ReportWriter: reportWriter,
Expand All @@ -358,7 +349,7 @@ func processOneImage(srcRegistry, destRegistry registry.RegistryOptions, image s
}

// copy image from local to remote
_, err = CopyImageWithGC(context.Background(), policyContext, destRef, localRef, &copy.Options{
_, err = CopyImageWithGC(context.Background(), destRef, localRef, &copy.Options{
RemoveSignatures: true,
SignBy: "",
ReportWriter: reportWriter,
Expand Down Expand Up @@ -428,15 +419,6 @@ func (ref *ImageRef) String() string {
}

func CopyFromFileToRegistry(path string, name string, tag string, digest string, auth RegistryAuth, reportWriter io.Writer) error {
policy, err := signature.NewPolicyFromBytes(imagePolicy)
if err != nil {
return errors.Wrap(err, "failed to read default policy")
}
policyContext, err := signature.NewPolicyContext(policy)
if err != nil {
return errors.Wrap(err, "failed to create policy")
}

srcRef, err := alltransports.ParseImageName(fmt.Sprintf("docker-archive:%s", path))
if err != nil {
return errors.Wrap(err, "failed to parse src image name")
Expand Down Expand Up @@ -472,7 +454,7 @@ func CopyFromFileToRegistry(path string, name string, tag string, digest string,
}
}

_, err = CopyImageWithGC(context.Background(), policyContext, destRef, srcRef, &copy.Options{
_, err = CopyImageWithGC(context.Background(), destRef, srcRef, &copy.Options{
RemoveSignatures: true,
SignBy: "",
ReportWriter: reportWriter,
Expand Down Expand Up @@ -600,7 +582,24 @@ func isTooManyRequests(err error) bool {
return isTooManyRequests(cause)
}

func CopyImageWithGC(ctx context.Context, policyContext *signature.PolicyContext, destRef, srcRef types.ImageReference, options *copy.Options) ([]byte, error) {
func getPolicyContext() (*signature.PolicyContext, error) {
policy, err := signature.NewPolicyFromBytes(imagePolicy)
if err != nil {
return nil, errors.Wrap(err, "failed to read default policy")
}
policyContext, err := signature.NewPolicyContext(policy)
if err != nil {
return nil, errors.Wrap(err, "failed to create policy")
}
return policyContext, nil
}

func CopyImageWithGC(ctx context.Context, destRef, srcRef types.ImageReference, options *copy.Options) ([]byte, error) {
policyContext, err := getPolicyContext()
if err != nil {
return nil, errors.Wrap(err, "failed to get policy")
}

manifest, err := copy.Image(ctx, policyContext, destRef, srcRef, options)

// copying an image increases allocated memory, which can push the pod to cross the memory limit when copying multiple images in a row.
Expand Down
132 changes: 132 additions & 0 deletions pkg/kotsadm/copy_images.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
package kotsadm

import (
"context"
"fmt"
"os"

"github.com/containers/image/v5/copy"
"github.com/containers/image/v5/transports/alltransports"
imagev5types "github.com/containers/image/v5/types"
"github.com/docker/distribution/reference"
"github.com/pkg/errors"
"github.com/replicatedhq/kots/pkg/docker/registry"
"github.com/replicatedhq/kots/pkg/image"
"github.com/replicatedhq/kots/pkg/k8sutil"
kotsadmobjects "github.com/replicatedhq/kots/pkg/kotsadm/objects"
"github.com/replicatedhq/kots/pkg/kotsadm/types"
kotsadmtypes "github.com/replicatedhq/kots/pkg/kotsadm/types"
"k8s.io/client-go/kubernetes"
)

// Copies Admin Console images from public registry to private registry
func CopyImages(sourceEndpoint string, options types.PushImagesOptions, kotsNamespace string) error {
clientset, err := k8sutil.GetClientset()
if err != nil {
return errors.Wrap(err, "failed to get clientset")
}

// Minimal info needed to get the right image names
deployOptions := kotsadmtypes.DeployOptions{
IsOpenShift: k8sutil.IsOpenShift(clientset),
KotsadmOptions: kotsadmtypes.KotsadmOptions{
OverrideRegistry: options.Registry.Endpoint,
OverrideNamespace: options.Registry.Namespace,
Username: options.Registry.Username,
Password: options.Registry.Password,
},
}

sourceImages := kotsadmobjects.GetOriginalAdminConsoleImages(deployOptions)
destImages := kotsadmobjects.GetAdminConsoleImages(deployOptions)
for imageName, sourceImage := range sourceImages {
destImage := destImages[imageName]
if destImage == "" {
return errors.Errorf("failed to find image %s in destination list", imageName)
}

writeProgressLine(options.ProgressWriter, fmt.Sprintf("Copying %s to %s", sourceImage, destImage))

sourceCtx, err := getCopyImagesSourceContext(clientset, sourceEndpoint, kotsNamespace)
if err != nil {
return errors.Wrap(err, "failed to get source context")
}

srcRef, err := alltransports.ParseImageName(fmt.Sprintf("docker://%s", sourceImage))
if err != nil {
return errors.Wrapf(err, "failed to parse source image name %s", sourceImage)
}

destStr := fmt.Sprintf("docker://%s", destImage)
destRef, err := alltransports.ParseImageName(destStr)
if err != nil {
return errors.Wrapf(err, "failed to parse dest image name %s", destStr)
}

destCtx := &imagev5types.SystemContext{
DockerInsecureSkipTLSVerify: imagev5types.OptionalBoolTrue,
DockerDisableV1Ping: true,
}

username, password := options.Registry.Username, options.Registry.Password
registryHost := reference.Domain(destRef.DockerReference())

if registry.IsECREndpoint(registryHost) && username != "AWS" {
login, err := registry.GetECRLogin(registryHost, username, password)
if err != nil {
return errors.Wrap(err, "failed to get ECR login")
}
username = login.Username
password = login.Password
}

if username != "" && password != "" {
destCtx.DockerAuthConfig = &imagev5types.DockerAuthConfig{
Username: username,
Password: password,
}
}

_, err = image.CopyImageWithGC(context.Background(), destRef, srcRef, &copy.Options{
RemoveSignatures: true,
SignBy: "",
ReportWriter: options.ProgressWriter,
SourceCtx: sourceCtx,
DestinationCtx: destCtx,
ForceManifestMIMEType: "",
})
if err != nil {
return errors.Wrapf(err, "failed to copy %s to %s: %v", sourceImage, destImage, err)
}
}

return nil
}

func getCopyImagesSourceContext(clientset kubernetes.Interface, sourceEndpoint string, kotsNamespace string) (*imagev5types.SystemContext, error) {
sourceCtx := &imagev5types.SystemContext{DockerDisableV1Ping: true}

// allow pulling images from http/invalid https docker repos
// intended for development only, _THIS MAKES THINGS INSECURE_
if os.Getenv("KOTSADM_INSECURE_SRCREGISTRY") == "true" {
sourceCtx.DockerInsecureSkipTLSVerify = imagev5types.OptionalBoolTrue
}

if sourceEndpoint != "" && sourceEndpoint != "docker.io" && sourceEndpoint != "index.docker.io" {
return sourceCtx, nil
}

credentials, err := registry.GetDockerHubCredentials(clientset, kotsNamespace)
if err != nil {
return nil, errors.Wrap(err, "failed to get docker hub credentials")
}

if credentials.Username != "" && credentials.Password != "" {
sourceCtx.DockerAuthConfig = &imagev5types.DockerAuthConfig{
Username: credentials.Username,
Password: credentials.Password,
}
}

return sourceCtx, nil
}
11 changes: 11 additions & 0 deletions pkg/kotsadm/objects/images.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,14 @@ func GetAdminConsoleImages(deployOptions types.DeployOptions) map[string]string
"dex": dexImage,
}
}

func GetOriginalAdminConsoleImages(deployOptions types.DeployOptions) map[string]string {
dexTag, _ := image.GetTag(image.Dex) // dex image is special; we host a copy
return map[string]string{
"kotsadm-migrations": fmt.Sprintf("kotsadm/kotsadm-migrations:%s", kotsadmversion.KotsadmTag(deployOptions.KotsadmOptions)),
"kotsadm": fmt.Sprintf("kotsadm/kotsadm:%s", kotsadmversion.KotsadmTag(deployOptions.KotsadmOptions)),
"minio": image.Minio,
"postgres": fmt.Sprintf("postgres:%s", getPostgresTag(deployOptions)),
"dex": fmt.Sprintf("kotsadm/dex:%s", dexTag),
}
}
17 changes: 2 additions & 15 deletions pkg/kotsadm/push_images.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ import (

"github.com/containers/image/v5/copy"
"github.com/containers/image/v5/docker/tarfile"
"github.com/containers/image/v5/signature"
"github.com/containers/image/v5/transports/alltransports"
containerstypes "github.com/containers/image/v5/types"
"github.com/pkg/errors"
Expand All @@ -28,6 +27,7 @@ import (
kustomizetypes "sigs.k8s.io/kustomize/api/types"
)

// Pushes Admin Console images from airgap bundle to private registry
func PushImages(airgapArchive string, options types.PushImagesOptions) error {
imagesRootDir, err := ioutil.TempDir("", "kotsadm-airgap")
if err != nil {
Expand Down Expand Up @@ -180,19 +180,6 @@ func processImageTags(rootDir string, format string, imageName string, options t
}

func pushOneImage(rootDir string, format string, imageName string, tag string, options types.PushImagesOptions) error {
var imagePolicy = []byte(`{
"default": [{"type": "insecureAcceptAnything"}]
}`)

policy, err := signature.NewPolicyFromBytes(imagePolicy)
if err != nil {
return errors.Wrap(err, "failed to read default policy")
}
policyContext, err := signature.NewPolicyContext(policy)
if err != nil {
return errors.Wrap(err, "failed to create policy")
}

destCtx := &containerstypes.SystemContext{
DockerInsecureSkipTLSVerify: containerstypes.OptionalBoolTrue,
DockerDisableV1Ping: true,
Expand Down Expand Up @@ -228,7 +215,7 @@ func pushOneImage(rootDir string, format string, imageName string, tag string, o

writeProgressLine(options.ProgressWriter, fmt.Sprintf("Pushing %s", destStr))

_, err = image.CopyImageWithGC(context.Background(), policyContext, destRef, localRef, &copy.Options{
_, err = image.CopyImageWithGC(context.Background(), destRef, localRef, &copy.Options{
RemoveSignatures: true,
SignBy: "",
ReportWriter: options.ProgressWriter,
Expand Down

0 comments on commit c9a7d70

Please sign in to comment.