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 }