Example #1
0
def _mk_transport(
    image_name: str,
    credentials_lookup: typing.Callable[[image_reference, oa.Privileges, bool],
                                        oa.OciConfig],
    action: str = docker_http.PULL,
    privileges: oa.Privileges = oa.Privileges.READONLY,
):
    if isinstance(image_name, str):
        image_name = ou.normalise_image_reference(image_name)
    credentials = _mk_credentials(
        image_reference=str(image_name),
        credentials_lookup=credentials_lookup,
        privileges=privileges,
    )
    if isinstance(image_name, str):
        image_name = docker_name.from_string(name=image_name)

    transport_pool = _mk_transport_pool(size=1)

    transport = docker_http.Transport(
        name=image_name,
        creds=credentials,
        transport=transport_pool,
        action=docker_http.PULL,
    )

    return transport
Example #2
0
def pulled_image(
    image_reference: str,
    credentials_lookup: typing.Callable[[image_reference, oa.Privileges, bool],
                                        oa.OciConfig],
):
    _tag_or_digest_reference(image_reference)

    transport = _mk_transport_pool()
    image_reference = ou.normalise_image_reference(image_reference)
    image_reference = docker_name.from_string(image_reference)
    creds = _mk_credentials(
        image_reference=image_reference,
        credentials_lookup=credentials_lookup,
    )

    # OCI Image Manifest is compatible with Docker Image Manifest Version 2,
    # Schema 2. We indicate support for both formats by passing both media types
    # as 'Accept' headers.
    #
    # For reference:
    #   OCI: https://github.com/opencontainers/image-spec
    #   Docker: https://docs.docker.com/registry/spec/manifest-v2-2/
    accept = docker_http.SUPPORTED_MANIFEST_MIMES

    try:
        logger.info(f'Pulling v2.2 image from {image_reference}..')
        with v2_2_image.FromRegistry(image_reference, creds, transport,
                                     accept) as v2_2_img:
            if v2_2_img.exists():
                yield v2_2_img
                return

        # XXX TODO: use streaming rather than writing to local FS
        # if outfile is given, we must use it instead of an ano
        logger.debug(f'Pulling manifest list from {image_reference}..')
        with image_list.FromRegistry(image_reference, creds,
                                     transport) as img_list:
            if img_list.exists():
                platform = image_list.Platform({
                    'architecture': _PROCESSOR_ARCHITECTURE,
                    'os': _OPERATING_SYSTEM,
                })
                # pytype: disable=wrong-arg-types
                with img_list.resolve(platform) as default_child:
                    yield default_child
                    return

        logger.info(f'Pulling v2 image from {image_reference}..')
        with v2_image.FromRegistry(image_reference, creds,
                                   transport) as v2_img:
            if v2_img.exists():
                with v2_compat.V22FromV2(v2_img) as v2_2_img:
                    yield v2_2_img
                    return

        raise om.OciImageNotFoundException(
            f'failed to retrieve {image_reference=} - does it exist?')

    except Exception as e:
        raise e
Example #3
0
def _mk_credentials(
        image_reference,
        credentials_lookup: typing.Callable[[image_reference, oa.Privileges],
                                            oa.OciConfig],
        privileges: oa.Privileges = None):
    if isinstance(image_reference, str):
        image_reference = docker_name.from_string(name=image_reference)
    try:
        # first try container_registry cfgs from available cfg
        creds = _credentials(
            image_reference=str(image_reference),
            credentials_lookup=credentials_lookup,
            privileges=privileges,
            absent_ok=True,
        )
        if not creds:
            logger.warning(f'could not find rw-creds for {image_reference}')
            # fall-back to default docker lookup
            creds = docker_creds.DefaultKeychain.Resolve(image_reference)

        return creds
    except Exception as e:
        logger.warning(
            f'Error resolving credentials for {image_reference}: {e}')
        raise e
Example #4
0
def _put_raw_image_manifest(
    image_reference: str,
    raw_contents: bytes,
    credentials_lookup: typing.Callable[[image_reference, oa.Privileges, bool],
                                        oa.OciConfig],
):
    image_name = docker_name.from_string(image_reference)

    push_sess = docker_session.Push(
        name=image_name,
        creds=_mk_credentials(
            image_reference=image_reference,
            credentials_lookup=credentials_lookup,
            privileges=oa.Privileges.READWRITE,
        ),
        transport=_mk_transport_pool(),
    )

    class ImageMock:
        def digest(self):
            return image_name.tag

        def manifest(self):
            return raw_contents

        def media_type(self):
            return docker_http.MANIFEST_SCHEMA2_MIME

    image_mock = ImageMock()

    push_sess._put_manifest(image=image_mock, use_digest=True)
Example #5
0
def tags(
    image_name: str,
    credentials_lookup: typing.Callable[[image_reference, oa.Privileges, bool],
                                        oa.OciConfig],
) -> typing.Sequence[str]:
    '''
    returns a sequence of all `tags` for the given image_name
    '''
    if isinstance(image_name, str):
        image_name = ou.normalise_image_reference(image_name)

    from containerregistry.client.v2_2 import docker_http
    transport = _ou._mk_transport(
        image_name=image_name,
        credentials_lookup=credentials_lookup,
        action=docker_http.PULL,
    )

    if isinstance(image_name, str):
        from containerregistry.client import docker_name
        image_name = docker_name.from_string(image_name)

    url = f'https://{image_name.registry}/v2/{image_name.repository}/tags/list'

    res, body_bytes = transport.Request(url, (200, ))
    parsed = json.loads(body_bytes)

    # XXX parsed['manifest'] might be used to e.g. determine stale images, and purge them
    tags = parsed['tags']
    return tags
Example #6
0
def _tag_or_digest_reference(image_reference):
    if isinstance(image_reference, str):
        image_reference = docker_name.from_string(image_reference)
    ref_type = type(image_reference)
    if ref_type in (docker_name.Tag, docker_name.Digest):
        return True
    raise ValueError(
        f'{image_reference=} is does not contain a symbolic or hash tag')
Example #7
0
def rm_tag(image_reference: str):
    transport = _mk_transport_pool()
    image_reference = ou.normalise_image_reference(image_reference)
    image_reference = docker_name.from_string(image_reference)
    creds = _mk_credentials(image_reference=image_reference)

    docker_session.Delete(
        name=image_reference,
        creds=creds,
        transport=transport,
    )
    logger.info(f'untagged {image_reference=} - note: did not purge blobs!')
Example #8
0
    def _image_ref_metadata(
        self,
        resource: gci.componentmodel.Resource,
        omit_version: bool,
    ):
        metadata_dict = {
            'IMAGE_REFERENCE_NAME': resource.name,
            'RESOURCE_TYPE': resource.type.value,
        }
        if not omit_version:
            image_ref_with_digest = docker_name.from_string(
                container.registry.to_hash_reference(
                    resource.access.imageReference, ))
            digest = f'@{image_ref_with_digest.digest}'
            metadata_dict['IMAGE_REFERENCE'] = resource.access.imageReference
            metadata_dict['IMAGE_VERSION'] = resource.version
            metadata_dict['IMAGE_DIGEST'] = digest
            metadata_dict['DIGEST_IMAGE_REFERENCE'] = str(
                image_ref_with_digest)

        return metadata_dict
Example #9
0
def _pull_image(
    image_reference: str,
    credentials_lookup: typing.Callable[[image_reference, oa.Privileges, bool],
                                        oa.OciConfig],
    outfileobj=None,
):
    if not image_reference:
        raise ValueError(image_reference)

    image_reference = ou.normalise_image_reference(
        image_reference=image_reference)

    outfileobj = outfileobj if outfileobj else tempfile.TemporaryFile()

    with tarfile.open(fileobj=outfileobj, mode='w:') as tar:
        with pulled_image(
                image_reference=image_reference,
                credentials_lookup=credentials_lookup,
        ) as image:
            image_reference = docker_name.from_string(image_reference)
            save.tarball(_make_tag_if_digest(image_reference), image, tar)
            return outfileobj
Example #10
0
def _push_image(image_reference: str,
                image_file: str,
                credentials_lookup: typing.Callable[
                    [image_reference, oa.Privileges, bool], oa.OciConfig],
                threads=8):
    if not image_reference:
        raise ValueError(image_reference)
    if not os.path.isfile(image_file):
        raise ValueError(f'not an exiting file: {image_file=}')

    transport = _mk_transport_pool()

    image_reference = ou.normalise_image_reference(image_reference)
    image_reference = docker_name.from_string(image_reference)

    creds = _mk_credentials(
        image_reference=image_reference,
        credentials_lookup=credentials_lookup,
        privileges=oa.Privileges.READWRITE,
    )
    # XXX fail if no creds were found

    with v2_2_image.FromTarball(image_file) as v2_2_img:
        try:
            with docker_session.Push(
                    image_reference,
                    creds,
                    transport,
                    threads=threads,
            ) as session:
                session.upload(v2_2_img)
                digest = v2_2_img.digest()
                logger.info(
                    f'{image_reference} was uploaded - digest: {digest}')
        except Exception as e:
            import traceback
            traceback.print_exc()
            raise e
Example #11
0
def to_hash_reference(image_name: str):
    transport = _mk_transport_pool(size=1)

    image_name = ou.normalise_image_reference(image_name)
    image_reference = docker_name.from_string(image_name)
    creds = _mk_credentials(image_reference=image_reference)
    accept = docker_http.SUPPORTED_MANIFEST_MIMES

    digest = None

    with image_list.FromRegistry(image_reference, creds,
                                 transport) as img_list:
        if img_list.exists():
            digest = img_list.digest()
        else:
            logger.debug('no manifest found')

    # look for image
    with v2_2_image.FromRegistry(image_reference, creds, transport,
                                 accept) as v2_2_img:
        if v2_2_img.exists():
            digest = v2_2_img.digest()
        else:
            logger.debug('no img v2.2 found')

    if not digest:
        # fallback to v2
        with v2_image.FromRegistry(image_reference, creds,
                                   transport) as v2_img:
            if v2_img.exists():
                digest = v2_img.digest()
            else:
                logger.debug('no img v2 found')
                raise RuntimeError(
                    f'could not access img-metadata for {image_name}')

    name = image_name.rsplit(':', 1)[0]
    return f'{name}@{digest}'
    def all(
            self,
            build_project,
            registry_project,
            remote_fork,  # pylint: disable=too-many-statements,too-many-branches
            kustomize_file,
            add_github_host=False):
        """Build the latest image and update the prototype.

    Args:
      build_project: GCP project used to build the image.
      registry_project: GCP project used to host the image.
      remote_fork: Url of the remote fork.
        The remote fork used to create the PR;
         e.g. [email protected]:jlewi/kubeflow.git. currently only ssh is
         supported.
      kustomize_file: Path to the kustomize file
      add_github_host: If true will add the github ssh host to known ssh hosts.
    """
        # TODO(jlewi): How can we automatically determine the root of the git
        # repo containing the kustomize_file?
        self.manifests_repo_dir = util.run(
            ["git", "rev-parse", "--show-toplevel"],
            cwd=os.path.dirname(kustomize_file))
        repo = git.Repo(self.manifests_repo_dir)
        util.maybe_activate_service_account()
        last_commit = self.last_commit

        # Ensure github.com is in the known hosts
        if add_github_host:
            output = util.run(["ssh-keyscan", "github.com"])
            with open(os.path.join(os.getenv("HOME"), ".ssh", "known_hosts"),
                      mode='a') as hf:
                hf.write(output)

        if not remote_fork.startswith("*****@*****.**"):
            raise ValueError("Remote fork currently only supports ssh")

        remote_repo = self._find_remote_repo(repo, remote_fork)

        if not remote_repo:
            fork_name = remote_fork.split(":", 1)[-1].split("/", 1)[0]
            logging.info("Adding remote %s=%s", fork_name, remote_fork)
            remote_repo = repo.create_remote(fork_name, remote_fork)

        logging.info("Last change to components-jupyter-web-app was %s",
                     last_commit)

        base = "gcr.io/{0}/jupyter-web-app".format(registry_project)

        # Check if there is already an image tagged with this commit.
        image = base + ":" + self.last_commit
        transport = transport_pool.Http(httplib2.Http)
        src = docker_name.from_string(image)
        creds = docker_creds.DefaultKeychain.Resolve(src)

        image_exists = False
        try:
            with v2_2_image.FromRegistry(src, creds, transport) as src_image:
                logging.info("Image %s exists; digest: %s", image,
                             src_image.digest())
                image_exists = True
        except docker_http.V2DiagnosticException as e:
            if e.status == 404:
                logging.info("%s doesn't exist", image)
            else:
                raise

        if not image_exists:
            logging.info("Building the image")
            image = self.build_image(build_project, registry_project)
            logging.info("Created image: %s", image)
        else:
            logging.info("Image %s already exists", image)

        # TODO(jlewi): What if the file was already modified so we didn't
        # modify it in this run but we still need to commit it?
        image_updated = application_util.set_kustomize_image(
            kustomize_file, JUPYTER_WEB_APP_IMAGE_NAME, image)

        if not image_updated:
            logging.info("kustomization not updated so not creating a PR.")
            return

        application_util.regenerate_manifest_tests(self.manifests_repo_dir)

        branch_name = "update_jupyter_{0}".format(last_commit)

        if repo.active_branch.name != branch_name:
            logging.info("Creating branch %s", branch_name)

            branch_names = [b.name for b in repo.branches]
            if branch_name in branch_names:
                logging.info("Branch %s exists", branch_name)
                util.run(["git", "checkout", branch_name],
                         cwd=self.manifests_repo_dir)
            else:
                util.run(["git", "checkout", "-b", branch_name],
                         cwd=self.manifests_repo_dir)

        if self._check_if_pr_exists(commit=last_commit):
            # Since a PR already exists updating to the specified commit
            # don't create a new one.
            # We don't want to just push -f because if the PR already exists
            # git push -f will retrigger the tests.
            # To force a recreate of the PR someone could close the existing
            # PR and a new PR will be created on the next cron run.
            return

        logging.info("Add file %s to repo", kustomize_file)
        repo.index.add([kustomize_file])
        repo.index.add([os.path.join(self.manifests_repo_dir, "tests/*")])
        repo.index.commit(
            "Update the jupyter web app image to {0}".format(image))

        util.run([
            "git", "push", "-f", remote_repo.name,
            "{0}:{0}".format(branch_name)
        ],
                 cwd=self.manifests_repo_dir)

        self.create_pull_request(commit=last_commit)
Example #13
0
  def all(self, build_project, registry_project, remote_fork,
          add_github_host=False): # pylint: disable=too-many-statements,too-many-branches
    """Build the latest image and update the prototype.

    Args:
      build_project: GCP project used to build the image.
      registry_project: GCP project used to host the image.
      remote_fork: Url of the remote fork.
        The remote fork used to create the PR;
         e.g. [email protected]:jlewi/kubeflow.git. currently only ssh is
         supported.
      add_github_host: If true will add the github ssh host to known ssh hosts.
    """
    repo = git.Repo(self._root_dir())
    util.maybe_activate_service_account()
    last_commit = self.last_commit

    # Ensure github.com is in the known hosts
    if add_github_host:
      output = util.run(["ssh-keyscan", "github.com"])
      with open(os.path.join(os.getenv("HOME"), ".ssh", "known_hosts"),
                mode='a') as hf:
        hf.write(output)

    if not remote_fork.startswith("*****@*****.**"):
      raise ValueError("Remote fork currently only supports ssh")

    remote_repo = self._find_remote_repo(repo, remote_fork)

    if not remote_repo:
      fork_name = remote_fork.split(":", 1)[-1].split("/", 1)[0]
      logging.info("Adding remote %s=%s", fork_name, remote_fork)
      remote_repo = repo.create_remote(fork_name, remote_fork)

    logging.info("Last change to components-jupyter-web-app was %s", last_commit)

    base = "gcr.io/{0}/jupyter-web-app".format(registry_project)

    # Check if there is already an image tagged with this commit.
    image = base + ":" + self.last_commit
    transport = transport_pool.Http(httplib2.Http)
    src = docker_name.from_string(image)
    creds = docker_creds.DefaultKeychain.Resolve(src)

    image_exists = False
    try:
      with v2_2_image.FromRegistry(src, creds, transport) as src_image:
        logging.info("Image %s exists; digest: %s", image,
                     src_image.digest())
        image_exists = True
    except docker_http.V2DiagnosticException as e:
      if e.status == 404:
        logging.info("%s doesn't exist", image)
      else:
        raise

    if not image_exists:
      logging.info("Building the image")
      image = self.build_image(build_project, registry_project)
      logging.info("Created image: %s", image)
    else:
      logging.info("Image %s already exists", image)

    # We should check what the current image is if and not update it
    # if its the existing image
    prototype_file = self.update_prototype(image)

    if not prototype_file:
      logging.info("Prototype not updated so not creating a PR.")
      return

    branch_name = "update_jupyter_{0}".format(last_commit)

    if repo.active_branch.name != branch_name:
      logging.info("Creating branch %s", branch_name)

      branch_names = [b.name for b in repo.branches]
      if branch_name in branch_names:
        logging.info("Branch %s exists", branch_name)
        util.run(["git", "checkout", branch_name], cwd=self._root_dir())
      else:
        util.run(["git", "checkout", "-b", branch_name], cwd=self._root_dir())

    logging.info("Add file %s to repo", prototype_file)
    repo.index.add([prototype_file])
    repo.index.commit("Update the jupyter web app image to {0}".format(image))

    util.run(["git", "push", "-f", remote_repo.name], cwd=self._root_dir())

    self.create_pull_request(commit=last_commit)
Example #14
0
    def upload_container_image_group(
        self,
        resource_group: ResourceGroup,
    ) -> typing.Iterable[UploadResult]:
        mk_upload_result = partial(
            UploadResult,
            component=resource_group.component(),
        )

        ci.util.info(
            f'Processing resource group for component {resource_group.component().name} and image '
            f'{resource_group.image_name()} with {len(resource_group.resources())} resources'
        )

        # depending on upload-mode, determine an upload-action for each related image
        # - images to upload
        # - protecode-apps to remove
        # - triages to import
        images_to_upload = list()
        protecode_apps_to_remove = set()
        protecode_apps_to_consider = list(
        )  # consider to rescan; return results
        triages_to_import = set()

        metadata = self._image_group_metadata(
            resource_group=resource_group,
            omit_version=True,
        )

        existing_products = self._api.list_apps(
            group_id=self._group_id,
            custom_attribs=metadata,
        )

        # import triages from local group
        scan_results = (self._api.scan_result(product_id=product.product_id())
                        for product in existing_products)
        triages_to_import |= set(self._existing_triages(scan_results))

        # import triages from reference groups
        def enumerate_reference_triages():
            for group_id in self._reference_group_ids:
                ref_apps = self._api.list_apps(
                    group_id=group_id,
                    custom_attribs=metadata,
                )
                ref_scan_results = (self._api.scan_result(app.product_id())
                                    for app in ref_apps)
                yield from self._existing_triages(ref_scan_results)

        triages_to_import |= set(enumerate_reference_triages())
        ci.util.info(f'found {len(triages_to_import)} triage(s) to import')

        if self._processing_mode is ProcessingMode.FORCE_UPLOAD:
            ci.util.info('force-upload - will re-upload all images')
            images_to_upload += list(resource_group.resources())
            # remove all
            protecode_apps_to_remove = set(existing_products)
        elif self._processing_mode is ProcessingMode.RESCAN:
            for resource in resource_group.resources():
                # find matching protecode product (aka app)
                for existing_product in existing_products:
                    product_image_digest = existing_product.custom_data().get(
                        'IMAGE_DIGEST')
                    container_image_digest = docker_name.from_string(
                        container.registry.to_hash_reference(
                            resource.access.imageReference, ))
                    if product_image_digest == f'@{container_image_digest.digest}':
                        existing_products.remove(existing_product)
                        protecode_apps_to_consider.append(existing_product)
                        break
                else:
                    ci.util.info(
                        f'did not find image {resource.access.imageReference} - will upload'
                    )
                    # not found -> need to upload
                    images_to_upload.append(resource)

            # all existing products that did not match shall be removed
            protecode_apps_to_remove |= set(existing_products)

        else:
            raise NotImplementedError()

        # trigger rescan if recommended
        for protecode_app in protecode_apps_to_consider:
            scan_result = self._api.scan_result_short(
                product_id=protecode_app.product_id())

            if not scan_result.is_stale():
                continue  # protecode does not recommend a rescan

            if not scan_result.has_binary():
                # scan_result here is an AnalysisResult which lacks our metadata. We need the
                # metadata to fetch the image version. Therefore, fetch the proper result
                scan_result = self._api.scan_result(
                    product_id=protecode_app.product_id())
                image_digest = scan_result.custom_data().get('IMAGE_DIGEST')
                # there should be at most one matching image (by image digest)
                for resource in resource_group:
                    container_image_digest = docker_name.from_string(
                        container.registry.to_hash_reference(
                            resource.access.imageReference, ))
                    if image_digest == f'@{container_image_digest.digest}':
                        ci.util.info(
                            f'File for container image "{resource.access.imageReference}" no '
                            'longer available to protecode - will upload')
                        images_to_upload.append(resource)
                        protecode_apps_to_consider.remove(protecode_app)
                        # xxx - also add app for removal?
                        break
            else:
                self._api.rescan(protecode_app.product_id())

        # upload new images
        for resource in images_to_upload:
            try:
                scan_result = self._upload_image(
                    component=resource_group.component(),
                    resource=resource,
                )
            except requests.exceptions.HTTPError as e:
                # in case the image is currently being scanned, Protecode will answer with HTTP
                # code 409 ('conflict'). In this case, fetch the ongoing scan to add it
                # to the list of scans to consider. In all other cases re-raise the error.
                if e.response.status_code != requests.codes.conflict:
                    raise e
                scan_result = self.retrieve_scan_result(
                    component=resource_group.component(),
                    resource=resource,
                )

            protecode_apps_to_consider.append(scan_result)

        # wait for all apps currently being scanned
        for protecode_app in protecode_apps_to_consider:
            # replace - potentially incomplete - scan result
            protecode_apps_to_consider.remove(protecode_app)
            ci.util.info(f'waiting for {protecode_app.product_id()}')
            protecode_apps_to_consider.append(
                self._api.wait_for_scan_result(protecode_app.product_id()))
            ci.util.info(f'finished waiting for {protecode_app.product_id()}')

        # apply imported triages for all protecode apps
        for protecode_app in protecode_apps_to_consider:
            product_id = protecode_app.product_id()
            scan_result = self._api.scan_result(product_id)
            existing_triages = list(self._existing_triages([scan_result]))
            new_triages = [
                t for t in triages_to_import if t not in existing_triages
            ]
            ci.util.info(
                f'transporting triages for {protecode_app.product_id()}')
            self._transport_triages(new_triages, product_id)
            ci.util.info(
                f'done with transporting triages for {protecode_app.product_id()}'
            )

        # apply triages from GCR
        protecode_apps_to_consider = [
            self._import_triages_from_gcr(protecode_app)
            for protecode_app in protecode_apps_to_consider
        ]

        # yield results
        for protecode_app in protecode_apps_to_consider:
            scan_result = self._api.scan_result(protecode_app.product_id())

            # create closure for pdf retrieval to avoid actually having to store
            # all the pdf-reports in memory. Will be called when preparing to send
            # the notification emails if reports are to be included
            def pdf_retrieval_function():
                return self._api.pdf_report(protecode_app.product_id())

            yield mk_upload_result(
                status=UploadStatus.DONE,  # XXX remove this
                result=scan_result,
                resource=resource,
                pdf_report_retrieval_func=pdf_retrieval_function,
            )

        # rm all outdated protecode apps
        for protecode_app in protecode_apps_to_remove:
            product_id = protecode_app.product_id()
            self._api.delete_product(product_id=product_id)
            ci.util.info(f'purged outdated product {product_id}')