github.com/codefresh-io/kcfi@v0.0.0-20230301195427-c1578715cc46/pkg/action/images.go (about)

     1  /*
     2  Copyright The Codefresh Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package action
    18  
    19  import (
    20  	"bufio"
    21  	"fmt"
    22  	"io"
    23  	"io/ioutil"
    24  	"log"
    25  	"os"
    26  	"path"
    27  	"regexp"
    28  	"strings"
    29  
    30  	"github.com/pkg/errors"
    31  	"github.com/stretchr/objx"
    32  
    33  	"github.com/google/go-containerregistry/pkg/authn"
    34  	clogs "github.com/google/go-containerregistry/pkg/logs"
    35  	"github.com/google/go-containerregistry/pkg/name"
    36  	"github.com/google/go-containerregistry/pkg/v1/remote"
    37  
    38  	c "github.com/codefresh-io/kcfi/pkg/config"
    39  )
    40  
    41  // ImagesPusher pusher of images
    42  type ImagesPusher struct {
    43  	// CfRegistryAuthConfig *authn.AuthConfig
    44  	// DstRegistryAuthConfig *authn.AuthConfig
    45  	Keychain    authn.Keychain
    46  	DstRegistry name.Registry
    47  	ImagesList  []string
    48  }
    49  
    50  // pusherKeychain implements authn.Keychain with the semantics of the standard Docker
    51  // credential keychain.
    52  type pusherKeychain struct {
    53  	cfRegistry            name.Registry
    54  	cfRegistryAuthConfig  *authn.AuthConfig
    55  	dstRegistry           name.Registry
    56  	dstRegistryAuthConfig *authn.AuthConfig
    57  }
    58  
    59  func init() {
    60  	//initialize go-conteinerregistry logger
    61  	clogs.Warn = log.New(os.Stderr, "", log.LstdFlags)
    62  	clogs.Progress = log.New(os.Stdout, "", log.LstdFlags)
    63  
    64  	if os.Getenv(c.EnvPusherDebug) != "" {
    65  		clogs.Debug = log.New(os.Stderr, "", log.LstdFlags)
    66  	}
    67  }
    68  
    69  func (k *pusherKeychain) Resolve(target authn.Resource) (authn.Authenticator, error) {
    70  
    71  	var authenticator authn.Authenticator
    72  	key := target.RegistryStr()
    73  	switch {
    74  	case key == name.DefaultRegistry:
    75  		authenticator = authn.Anonymous
    76  	case key == k.cfRegistry.RegistryStr():
    77  		authenticator = authn.FromConfig(*k.cfRegistryAuthConfig)
    78  	case key == k.dstRegistry.RegistryStr():
    79  		authenticator = authn.FromConfig(*k.dstRegistryAuthConfig)
    80  	default:
    81  		authenticator = authn.Anonymous
    82  	}
    83  
    84  	return authenticator, nil
    85  }
    86  
    87  func NewImagesPusherFromConfig(config map[string]interface{}) (*ImagesPusher, error) {
    88  
    89  	cfgX := objx.New(config)
    90  	baseDir := cfgX.Get(c.KeyBaseDir).String()
    91  
    92  	// get AuthConfig Codefresh Enterprise registry
    93  	cfRegistry, _ := name.NewRegistry(c.CfRegistryAddress)
    94  	var cfRegistryAuthConfig *authn.AuthConfig
    95  	cfRegistrySaVal := cfgX.Get(c.KeyImagesCodefreshRegistrySa).Str("")
    96  	if cfRegistrySaVal != "" {
    97  		cfRegistrySaPath := path.Join(baseDir, cfRegistrySaVal)
    98  		cfRegistryPasswordB, err := ioutil.ReadFile(cfRegistrySaPath)
    99  		if err != nil {
   100  			return nil, errors.Wrap(err, fmt.Sprintf("cannot read %s", cfRegistrySaPath))
   101  		}
   102  		cfRegistryAuthConfig = &authn.AuthConfig{
   103  			Username: c.CfRegistryUsername,
   104  			Password: string(cfRegistryPasswordB),
   105  		}
   106  	} else {
   107  		info("Warning: Codefresh registry credentials are not set")
   108  		cfRegistryAuthConfig = &authn.AuthConfig{}
   109  	}
   110  
   111  	// get AuthConfig for destination provate registry
   112  	dstRegistryAddress := cfgX.Get(c.KeyImagesPrivateRegistryAddress).String()
   113  	dstRegistry, err := name.NewRegistry(dstRegistryAddress)
   114  	if err != nil {
   115  		return nil, errors.Wrapf(err, "invalid registry address %s", dstRegistryAddress)
   116  	}
   117  	dstRegistryUsername := cfgX.Get(c.KeyImagesPrivateRegistryUsername).String()
   118  	dstRegistryPassword := cfgX.Get(c.KeyImagesPrivateRegistryPassword).String()
   119  	if len(dstRegistryAddress) == 0 || len(dstRegistryUsername) == 0 || len(dstRegistryPassword) == 0 {
   120  		err = fmt.Errorf("missing private registry data: ")
   121  		if len(dstRegistryAddress) == 0 {
   122  			err = errors.Wrapf(err, "missing %s", c.KeyImagesPrivateRegistryAddress)
   123  		}
   124  		if len(dstRegistryUsername) == 0 {
   125  			err = errors.Wrapf(err, "missing %s", c.KeyImagesPrivateRegistryUsername)
   126  		}
   127  		if len(dstRegistryPassword) == 0 {
   128  			err = errors.Wrapf(err, "missing %s", c.KeyImagesPrivateRegistryPassword)
   129  		}
   130  		return nil, err
   131  	}
   132  
   133  	dstRegistryAuthConfig := &authn.AuthConfig{
   134  		Username: dstRegistryUsername,
   135  		Password: dstRegistryPassword,
   136  	}
   137  
   138  	keychain := &pusherKeychain{
   139  		cfRegistry:            cfRegistry,
   140  		cfRegistryAuthConfig:  cfRegistryAuthConfig,
   141  		dstRegistry:           dstRegistry,
   142  		dstRegistryAuthConfig: dstRegistryAuthConfig,
   143  	}
   144  
   145  	// Get Images List
   146  	var imagesListsFiles, imagesList []string
   147  	// cfgX.Get(c.KeyImagesLists).StrSlice() - not working, returns empty
   148  	imagesListsFilesI := cfgX.Get(c.KeyImagesLists).Data()
   149  	if fileNamesI, ok := imagesListsFilesI.([]interface{}); ok {
   150  		for _, f := range fileNamesI {
   151  			if str, isStr := f.(string); isStr {
   152  				imagesListsFiles = append(imagesListsFiles, str)
   153  			} else {
   154  				info("Warning: %s - %v is not a string", c.KeyImagesLists, f)
   155  			}
   156  		}
   157  		debug("%v - %v", imagesListsFilesI, imagesListsFiles)
   158  	} else if imagesListsFilesI != nil {
   159  		info("Warning: %s - %v is not a list", c.KeyImagesLists, imagesListsFilesI)
   160  	}
   161  
   162  	for _, imagesListFile := range imagesListsFiles {
   163  		imagesListF, err := ReadListFile(path.Join(baseDir, imagesListFile))
   164  		if err != nil {
   165  			info("Error: failed to read %s - %v", imagesListFile, err)
   166  			continue
   167  		}
   168  		for _, image := range imagesListF {
   169  			imagesList = append(imagesList, image)
   170  		}
   171  	}
   172  
   173  	return &ImagesPusher{
   174  		DstRegistry: dstRegistry,
   175  		Keychain:    keychain,
   176  		ImagesList:  imagesList,
   177  	}, nil
   178  }
   179  
   180  func (o *ImagesPusher) Run(images []string) error {
   181  	info("Running images pusher")
   182  	if len(images) == 0 {
   183  		info("No images to push")
   184  		return nil
   185  		// if len(o.ImagesList) == 0 {
   186  		// 	info("No images to push")
   187  		// 	return nil
   188  		// }
   189  		// images = o.ImagesList
   190  	}
   191  	imagesWarnings := make(map[string]string)
   192  
   193  	for _, imgName := range images {
   194  		info("\n------------------\nSource Image: %s", imgName)
   195  		imgRef, err := name.ParseReference(imgName)
   196  		if err != nil {
   197  			imagesWarnings[imgName] = fmt.Sprintf("cannot parse %s - %v", imgName, err)
   198  			info("Warning: %s", imagesWarnings[imgName])
   199  			continue
   200  		}
   201  
   202  		// Calculating destination image
   203  		/* there are 3 types of image names:
   204  		# 1. non-codefresh like bitnami/mongo:4.2 || k8s.gcr.io/ingress-nginx/controller:v1.2.0 - convert to private-registry-addr/bitnami/mongo:4.2 || private-registry-addr/ingress-nginx/controller:v1.2.0
   205  		# 2. codefresh public images like codefresh/engine:1.147.8 - convert to private-registry-addr/codefresh/engine:1.147.8
   206  		# 3. codefresh private images like gcr.io/codefresh-enterprise/codefresh/cf-api:21.153.1 || gcr.io/codefresh-inc/codefresh-io/argo-platform-api-graphql:1.1175.0 - convert to private-registry-addr/codefresh/cf-api:21.153.1 || private-registry-addr/codefresh/argo-platform-api-graphql:1.1175.0
   207  		# DELIMITERS = 'codefresh || codefresh-io'
   208  		*/
   209  		var dstImageName string
   210  		imgNameSplit := regexp.MustCompile(`(codefresh\/|codefresh-io\/)`).Split(imgName, -1)
   211  		if len(imgNameSplit) == 1 {
   212  			dstImageName = fmt.Sprintf("%s/%s", o.DstRegistry.RegistryStr(), imgName)
   213  			dstImageName = regexp.MustCompile(`(docker.io\/|k8s.gcr.io\/|registry.k8s.io\/|ghcr.io\/)`).ReplaceAllString(dstImageName, "")
   214  		} else if len(imgNameSplit) == 2 {
   215  			dstImageName = fmt.Sprintf("%s/codefresh/%s", o.DstRegistry.RegistryStr(), imgNameSplit[1])
   216  		} else {
   217  			imagesWarnings[imgName] = fmt.Sprintf("cannot convert image name %s to destination image", imgName)
   218  			info("Error: %s", imagesWarnings[imgName])
   219  			continue
   220  		}
   221  		dstRef, err := name.ParseReference(dstImageName)
   222  		if err != nil {
   223  			imagesWarnings[imgName] = fmt.Sprintf("cannot parse %s - %v", dstImageName, err)
   224  			info("Error: %s", imagesWarnings[imgName])
   225  			continue
   226  		}
   227  
   228  		img, err := remote.Image(imgRef, remote.WithAuthFromKeychain(o.Keychain))
   229  		if err != nil {
   230  			imagesWarnings[imgName] = fmt.Sprintf("cannot get source image %s - %v", imgName, err)
   231  			info("Error: %s", imagesWarnings[imgName])
   232  			continue
   233  		}
   234  
   235  		info("Dest.  Image: %s", dstImageName)
   236  		err = remote.Write(dstRef, img, remote.WithAuthFromKeychain(o.Keychain))
   237  		if err != nil {
   238  			imagesWarnings[imgName] = fmt.Sprintf("failed  %s to %s - %v", imgName, dstImageName, err)
   239  			info("Error: %s", imagesWarnings[imgName])
   240  			continue
   241  		}
   242  	}
   243  
   244  	cntProcessed := len(images)
   245  	cnfFail := len(imagesWarnings)
   246  	cntSucess := cntProcessed - cnfFail
   247  	if len(imagesWarnings) > 0 {
   248  		info("\n----- %d images were failed:", cnfFail)
   249  		for img, errMsg := range imagesWarnings {
   250  			info("%s - %s\n", img, errMsg)
   251  		}
   252  	}
   253  	info("\n----- Completed! -----\n%d of %d images were successfully pushed", cntSucess, cntProcessed)
   254  
   255  	return nil
   256  }
   257  
   258  // ReadListFile - reads file and returns list of strings with strimmed lines without #-comments, empty lines
   259  func ReadListFile(fileName string) ([]string, error) {
   260  	debug("Reading List File %s", fileName)
   261  	lines := []string{}
   262  	file, err := os.Open(fileName)
   263  	defer file.Close()
   264  	if err != nil {
   265  		return nil, errors.Wrapf(err, "failed to open file %s", fileName)
   266  	}
   267  	reader := bufio.NewReader(file)
   268  
   269  	commentLineRe, _ := regexp.Compile(`^ *#+.*$`)
   270  	nonEmptyLineRe, _ := regexp.Compile(`[a-zA-Z0-9]`)
   271  	for {
   272  		lineB, prefix, err := reader.ReadLine()
   273  		if err != nil {
   274  			if err == io.EOF {
   275  				break
   276  			} else {
   277  				return nil, errors.Wrapf(err, "failed to read file %s", fileName)
   278  			}
   279  		}
   280  		if prefix {
   281  			info("Warning: too long lines in %s", fileName)
   282  			continue
   283  		}
   284  		line := string(lineB)
   285  		if commentLineRe.MatchString(line) || !nonEmptyLineRe.MatchString(line) {
   286  			continue
   287  		}
   288  		lines = append(lines, strings.Trim(line, " "))
   289  	}
   290  	if len(lines) == 0 {
   291  		info("Warning: no valid lines in file %s", fileName)
   292  	}
   293  	return lines, nil
   294  }