|
| 1 | +$stdout.sync = true |
| 2 | + |
| 3 | +require 'json' |
| 4 | +require 'base64' |
| 5 | +require 'logger' |
| 6 | +require 'open3' |
| 7 | +require 'aws-sdk-ecr' |
| 8 | +require 'aws-sdk-ecrpublic' |
| 9 | + |
| 10 | +module SkopeoCopy |
| 11 | + ECR_PRIVATE_PATTERN = /\A(\d+)\.dkr\.ecr\.([^.]+)\.amazonaws\.com\z/ |
| 12 | + ECR_PUBLIC_HOST = 'public.ecr.aws' |
| 13 | + AUTHFILE = '/tmp/containers-auth.json' |
| 14 | + |
| 15 | + Registry = Data.define(:host, :kind, :account_id, :region) |
| 16 | + |
| 17 | + def self.detect_registry(image_ref) |
| 18 | + host = image_ref.sub(%r{\A[a-z0-9+-]+://}, '').split('/').first |
| 19 | + case host |
| 20 | + when ECR_PRIVATE_PATTERN |
| 21 | + Registry.new(host:, kind: :ecr_private, account_id: $1, region: $2) |
| 22 | + when ECR_PUBLIC_HOST |
| 23 | + Registry.new(host:, kind: :ecr_public, account_id: nil, region: nil) |
| 24 | + end |
| 25 | + end |
| 26 | + |
| 27 | + def self.ecr_login_password(account_id:, region:) |
| 28 | + client = Aws::ECR::Client.new(region:, logger:) |
| 29 | + resp = client.get_authorization_token(registry_ids: [account_id]) |
| 30 | + token = Base64.decode64(resp.authorization_data[0].authorization_token) |
| 31 | + _user, password = token.split(':', 2) |
| 32 | + password |
| 33 | + end |
| 34 | + |
| 35 | + # ECR Public endpoint is only available in us-east-1 |
| 36 | + def self.ecr_public_login_password |
| 37 | + client = Aws::ECRPublic::Client.new(region: 'us-east-1', logger:) |
| 38 | + resp = client.get_authorization_token |
| 39 | + token = Base64.decode64(resp.authorization_data.authorization_token) |
| 40 | + _user, password = token.split(':', 2) |
| 41 | + password |
| 42 | + end |
| 43 | + |
| 44 | + def self.skopeo_login(registry:, password:) |
| 45 | + out, status = Open3.capture2e('skopeo', 'login', '--authfile', AUTHFILE, '--username', 'AWS', '--password-stdin', registry, stdin_data: password) |
| 46 | + logger.info("skopeo login #{registry}: #{out}") |
| 47 | + raise "skopeo login #{registry} failed (status=#{status.exitstatus}): #{out}" unless status.success? |
| 48 | + end |
| 49 | + |
| 50 | + def self.skopeo_copy(src:, dst:) |
| 51 | + logger.info("skopeo copy #{src} #{dst}") |
| 52 | + out, status = Open3.capture2e('skopeo', 'copy', '--authfile', AUTHFILE, src, dst) |
| 53 | + logger.info("skopeo copy: #{out}") |
| 54 | + raise "skopeo copy failed (status=#{status.exitstatus}): #{out}" unless status.success? |
| 55 | + end |
| 56 | + |
| 57 | + def self.logger |
| 58 | + @logger ||= Logger.new($stdout) |
| 59 | + end |
| 60 | + |
| 61 | + def self.perform(event) |
| 62 | + params = event.fetch('skopeo_copy') |
| 63 | + src = params.fetch('src') |
| 64 | + dst = params.fetch('dst') |
| 65 | + |
| 66 | + registries = [detect_registry(src), detect_registry(dst)].compact.uniq |
| 67 | + registries.each do |reg| |
| 68 | + case reg.kind |
| 69 | + when :ecr_private |
| 70 | + password = ecr_login_password(account_id: reg.account_id, region: reg.region) |
| 71 | + skopeo_login(registry: reg.host, password:) |
| 72 | + when :ecr_public |
| 73 | + password = ecr_public_login_password |
| 74 | + skopeo_login(registry: reg.host, password:) |
| 75 | + end |
| 76 | + end |
| 77 | + |
| 78 | + skopeo_copy(src:, dst:) |
| 79 | + |
| 80 | + {status: 'ok', src:, dst:} |
| 81 | + end |
| 82 | +end |
| 83 | + |
| 84 | +def handler(event:, context:) |
| 85 | + SkopeoCopy.perform(event) |
| 86 | +end |
0 commit comments