|
| 1 | +package dockerurl |
| 2 | + |
| 3 | +import ( |
| 4 | + "context" |
| 5 | + "crypto/sha256" |
| 6 | + "crypto/sha512" |
| 7 | + "fmt" |
| 8 | + "hash" |
| 9 | + "io" |
| 10 | + "io/ioutil" |
| 11 | + "net/http" |
| 12 | + "os" |
| 13 | + "strings" |
| 14 | + |
| 15 | + "github.com/google/go-github/v32/github" |
| 16 | + "github.com/moby/buildkit/frontend/dockerfile/parser" |
| 17 | + "github.com/sirupsen/logrus" |
| 18 | + "github.com/thepwagner/action-update-docker/docker" |
| 19 | + "github.com/thepwagner/action-update/updater" |
| 20 | +) |
| 21 | + |
| 22 | +func (u *Updater) ApplyUpdate(ctx context.Context, update updater.Update) error { |
| 23 | + _, name := parseGitHubRelease(update.Path) |
| 24 | + nameUpper := strings.ToUpper(name) |
| 25 | + |
| 26 | + // Potential keys the version/release hash might be stored under: |
| 27 | + versionKeys := []string{ |
| 28 | + fmt.Sprintf("%s_VERSION", nameUpper), |
| 29 | + fmt.Sprintf("%s_RELEASE", nameUpper), |
| 30 | + } |
| 31 | + hashKeys := []string{ |
| 32 | + fmt.Sprintf("%s_SHASUM", nameUpper), |
| 33 | + fmt.Sprintf("%s_SHA256SUM", nameUpper), |
| 34 | + fmt.Sprintf("%s_SHA512SUM", nameUpper), |
| 35 | + fmt.Sprintf("%s_CHECKSUM", nameUpper), |
| 36 | + } |
| 37 | + |
| 38 | + return docker.WalkDockerfiles(u.root, func(path string, parsed *parser.Result) error { |
| 39 | + patterns := u.collectPatterns(ctx, parsed, versionKeys, hashKeys, update) |
| 40 | + logrus.WithFields(logrus.Fields{ |
| 41 | + "path": path, |
| 42 | + "patterns": len(patterns), |
| 43 | + }).Debug("collected patterns") |
| 44 | + return updateDockerfile(path, patterns) |
| 45 | + }) |
| 46 | +} |
| 47 | + |
| 48 | +func (u *Updater) collectPatterns(ctx context.Context, parsed *parser.Result, versionKeys, hashKeys []string, update updater.Update) map[string]string { |
| 49 | + i := docker.NewInterpolation(parsed) |
| 50 | + patterns := map[string]string{} |
| 51 | + var versionHit bool |
| 52 | + for _, k := range versionKeys { |
| 53 | + prev, ok := i.Vars[k] |
| 54 | + if !ok { |
| 55 | + continue |
| 56 | + } |
| 57 | + switch prev { |
| 58 | + case update.Previous: |
| 59 | + logrus.WithFields(logrus.Fields{ |
| 60 | + "key": k, |
| 61 | + "prefix": true, |
| 62 | + }).Debug("identified version key") |
| 63 | + patterns[fmt.Sprintf("%s=%s", k, prev)] = fmt.Sprintf("%s=%s", k, update.Next) |
| 64 | + versionHit = true |
| 65 | + case update.Previous[1:]: |
| 66 | + logrus.WithFields(logrus.Fields{ |
| 67 | + "key": k, |
| 68 | + "prefix": false, |
| 69 | + }).Debug("identified version key") |
| 70 | + patterns[fmt.Sprintf("%s=%s", k, update.Previous[1:])] = fmt.Sprintf("%s=%s", k, update.Next[1:]) |
| 71 | + versionHit = true |
| 72 | + } |
| 73 | + } |
| 74 | + if !versionHit { |
| 75 | + return patterns |
| 76 | + } |
| 77 | + |
| 78 | + for _, k := range hashKeys { |
| 79 | + prev, ok := i.Vars[k] |
| 80 | + if !ok { |
| 81 | + continue |
| 82 | + } |
| 83 | + log := logrus.WithField("key", k) |
| 84 | + log.Debug("identified hash key") |
| 85 | + |
| 86 | + newHash, err := u.updatedHash(ctx, update, prev) |
| 87 | + if err != nil { |
| 88 | + log.WithError(err).Warn("fetching updated hash") |
| 89 | + } else if newHash != "" { |
| 90 | + log.Debug("updated hash key") |
| 91 | + patterns[fmt.Sprintf("%s=%s", k, prev)] = fmt.Sprintf("%s=%s", k, newHash) |
| 92 | + } |
| 93 | + } |
| 94 | + return patterns |
| 95 | +} |
| 96 | + |
| 97 | +func (u *Updater) updatedHash(ctx context.Context, update updater.Update, oldHash string) (string, error) { |
| 98 | + // Fetch the previous release: |
| 99 | + owner, repoName := parseGitHubRelease(update.Path) |
| 100 | + prevRelease, _, err := u.ghRepos.GetReleaseByTag(ctx, owner, repoName, update.Previous) |
| 101 | + if err != nil { |
| 102 | + return "", fmt.Errorf("fetching previous release: %w", err) |
| 103 | + } |
| 104 | + |
| 105 | + // First pass, does the project release a SHASUMS etc file we can grab? |
| 106 | + for _, prevAsset := range prevRelease.Assets { |
| 107 | + log := logrus.WithField("name", prevAsset.GetName()) |
| 108 | + oldAsset, err := u.isShasumAsset(ctx, prevAsset, oldHash) |
| 109 | + if err != nil { |
| 110 | + log.WithError(err).Warn("inspecting potential hash asset") |
| 111 | + continue |
| 112 | + } else if len(oldAsset) == 0 { |
| 113 | + log.Debug("old shasum asset not found") |
| 114 | + continue |
| 115 | + } |
| 116 | + log.Debug("identified shasum asset in previous release") |
| 117 | + |
| 118 | + // The previous release contained a shasum file that contained the previous hash |
| 119 | + // Does the new release have the same file? |
| 120 | + newHash, err := u.updatedHashFromShasumAsset(ctx, prevAsset, oldAsset, oldHash, update) |
| 121 | + if err != nil { |
| 122 | + log.WithError(err).Warn("fetching updated hash asset") |
| 123 | + continue |
| 124 | + } |
| 125 | + if newHash != "" { |
| 126 | + log.Debug("fetched corresponding shasum asset from new release") |
| 127 | + return newHash, nil |
| 128 | + } |
| 129 | + } |
| 130 | + |
| 131 | + // There are no shasum files - get downloading |
| 132 | + logrus.Debug("shasum file not found, searching files from previous release") |
| 133 | + for _, prevAsset := range prevRelease.Assets { |
| 134 | + log := logrus.WithField("name", prevAsset.GetName()) |
| 135 | + h, err := u.isHashAsset(ctx, prevAsset, oldHash) |
| 136 | + if err != nil { |
| 137 | + log.WithError(err).Warn("checking hash of previous assets") |
| 138 | + continue |
| 139 | + } else if !h { |
| 140 | + continue |
| 141 | + } |
| 142 | + log.Debug("identified hashed asset in previous release") |
| 143 | + |
| 144 | + // This asset from a previous release matched the previous hash |
| 145 | + // Does the new release have the same file? |
| 146 | + newHash, err := u.updatedHashFromAsset(ctx, prevAsset, update, oldHash) |
| 147 | + if err != nil { |
| 148 | + return "", err |
| 149 | + } |
| 150 | + if newHash != "" { |
| 151 | + log.Debug("fetched corresponding asset from new release") |
| 152 | + return newHash, nil |
| 153 | + } |
| 154 | + } |
| 155 | + |
| 156 | + return "", nil |
| 157 | +} |
| 158 | + |
| 159 | +// isShasumAsset returns true if the release asset is a SHASUMS file containing the previous hash |
| 160 | +func (u *Updater) isShasumAsset(ctx context.Context, asset *github.ReleaseAsset, oldHash string) ([]string, error) { |
| 161 | + if asset.GetSize() > 1024 { |
| 162 | + return nil, nil |
| 163 | + } |
| 164 | + |
| 165 | + req, err := http.NewRequest("GET", asset.GetBrowserDownloadURL(), nil) |
| 166 | + if err != nil { |
| 167 | + return nil, err |
| 168 | + } |
| 169 | + req = req.WithContext(ctx) |
| 170 | + |
| 171 | + res, err := u.http.Do(req) |
| 172 | + if err != nil { |
| 173 | + return nil, err |
| 174 | + } |
| 175 | + defer res.Body.Close() |
| 176 | + b, err := ioutil.ReadAll(res.Body) |
| 177 | + if err != nil { |
| 178 | + return nil, err |
| 179 | + } |
| 180 | + s := string(b) |
| 181 | + if !strings.Contains(s, oldHash) { |
| 182 | + return nil, nil |
| 183 | + } |
| 184 | + return strings.Split(s, "\n"), nil |
| 185 | +} |
| 186 | + |
| 187 | +func (u *Updater) updatedHashFromShasumAsset(ctx context.Context, asset *github.ReleaseAsset, oldContents []string, oldHash string, update updater.Update) (string, error) { |
| 188 | + res, err := u.getUpdatedAsset(ctx, asset, update) |
| 189 | + if err != nil { |
| 190 | + return "", err |
| 191 | + } |
| 192 | + defer res.Body.Close() |
| 193 | + b, err := ioutil.ReadAll(res.Body) |
| 194 | + if err != nil { |
| 195 | + return "", err |
| 196 | + } |
| 197 | + s := string(b) |
| 198 | + |
| 199 | + // If there's one line, extract the checksum and return it: |
| 200 | + if len(oldContents) == 1 { |
| 201 | + return strings.SplitN(s, " ", 2)[0], nil |
| 202 | + } |
| 203 | + |
| 204 | + // If there's multiple lines, find the file corresponding the old hash: |
| 205 | + var hashedFile string |
| 206 | + for _, oldLine := range oldContents { |
| 207 | + split := strings.SplitN(oldLine, " ", 2) |
| 208 | + if split[0] == oldHash { |
| 209 | + hashedFile = split[1] |
| 210 | + } |
| 211 | + } |
| 212 | + if hashedFile == "" { |
| 213 | + return "", nil |
| 214 | + } |
| 215 | + |
| 216 | + logrus.WithField("fn", hashedFile).Debug("identified hashed file in shasum asset") |
| 217 | + for _, newLine := range strings.Split(s, "\n") { |
| 218 | + split := strings.SplitN(newLine, " ", 2) |
| 219 | + if len(split) == 1 { |
| 220 | + continue |
| 221 | + } |
| 222 | + if split[1] == hashedFile { |
| 223 | + return split[0], nil |
| 224 | + } |
| 225 | + } |
| 226 | + |
| 227 | + return "", nil |
| 228 | +} |
| 229 | + |
| 230 | +func (u *Updater) getUpdatedAsset(ctx context.Context, asset *github.ReleaseAsset, update updater.Update) (*http.Response, error) { |
| 231 | + newURL := asset.GetBrowserDownloadURL() |
| 232 | + newURL = strings.ReplaceAll(newURL, update.Previous, update.Next) |
| 233 | + newURL = strings.ReplaceAll(newURL, update.Previous[1:], update.Next[1:]) |
| 234 | + req, err := http.NewRequest("GET", newURL, nil) |
| 235 | + if err != nil { |
| 236 | + return nil, err |
| 237 | + } |
| 238 | + req = req.WithContext(ctx) |
| 239 | + return u.http.Do(req) |
| 240 | +} |
| 241 | + |
| 242 | +func (u *Updater) isHashAsset(ctx context.Context, asset *github.ReleaseAsset, oldHash string) (bool, error) { |
| 243 | + h, ok := hasher(oldHash) |
| 244 | + if !ok { |
| 245 | + return false, nil |
| 246 | + } |
| 247 | + |
| 248 | + req, err := http.NewRequest("GET", asset.GetBrowserDownloadURL(), nil) |
| 249 | + if err != nil { |
| 250 | + return false, err |
| 251 | + } |
| 252 | + req = req.WithContext(ctx) |
| 253 | + |
| 254 | + res, err := u.http.Do(req) |
| 255 | + if err != nil { |
| 256 | + return false, err |
| 257 | + } |
| 258 | + defer res.Body.Close() |
| 259 | + if _, err := io.Copy(h, res.Body); err != nil { |
| 260 | + return false, err |
| 261 | + } |
| 262 | + sum := h.Sum(nil) |
| 263 | + return fmt.Sprintf("%x", sum) == oldHash, nil |
| 264 | +} |
| 265 | + |
| 266 | +func hasher(oldHash string) (hash.Hash, bool) { |
| 267 | + switch len(oldHash) { |
| 268 | + case 64: |
| 269 | + return sha256.New(), true |
| 270 | + case 128: |
| 271 | + return sha512.New(), true |
| 272 | + default: |
| 273 | + return nil, false |
| 274 | + } |
| 275 | +} |
| 276 | + |
| 277 | +func (u *Updater) updatedHashFromAsset(ctx context.Context, asset *github.ReleaseAsset, update updater.Update, oldHash string) (string, error) { |
| 278 | + res, err := u.getUpdatedAsset(ctx, asset, update) |
| 279 | + if err != nil { |
| 280 | + return "", err |
| 281 | + } |
| 282 | + defer res.Body.Close() |
| 283 | + h, _ := hasher(oldHash) |
| 284 | + if _, err := io.Copy(h, res.Body); err != nil { |
| 285 | + return "", err |
| 286 | + } |
| 287 | + sum := h.Sum(nil) |
| 288 | + return fmt.Sprintf("%x", sum), nil |
| 289 | +} |
| 290 | + |
| 291 | +func updateDockerfile(path string, patterns map[string]string) error { |
| 292 | + // Buffer contents as a string |
| 293 | + b, err := ioutil.ReadFile(path) |
| 294 | + if err != nil { |
| 295 | + return fmt.Errorf("reading file: %w", err) |
| 296 | + } |
| 297 | + s := string(b) |
| 298 | + |
| 299 | + for old, replace := range patterns { |
| 300 | + s = strings.ReplaceAll(s, old, replace) |
| 301 | + } |
| 302 | + |
| 303 | + stat, err := os.Stat(path) |
| 304 | + if err != nil { |
| 305 | + return fmt.Errorf("stating file: %w", err) |
| 306 | + } |
| 307 | + if err := ioutil.WriteFile(path, []byte(s), stat.Mode()); err != nil { |
| 308 | + return fmt.Errorf("writing updated file: %w", err) |
| 309 | + } |
| 310 | + return nil |
| 311 | +} |
0 commit comments