Skip to content

Commit

Permalink
Merge pull request Azure#1 from snehapar9/snehapar/patch-list
Browse files Browse the repository at this point in the history
containerapp patch list demo
  • Loading branch information
daniv-msft authored May 5, 2023
2 parents bf132da + 1821286 commit 2b5cb59
Show file tree
Hide file tree
Showing 6 changed files with 201 additions and 3 deletions.
12 changes: 12 additions & 0 deletions src/containerapp/azext_containerapp/_help.py
Original file line number Diff line number Diff line change
Expand Up @@ -1271,3 +1271,15 @@
--environment MyContainerappEnv \\
--compose-file-path "path/to/docker-compose.yml"
"""

helps['containerapp patch list'] = """
type: command
short-summary: List patchable and unpatchable container apps.
examples:
- name: List patchable container apps.
text: |
az containerapp list -g MyResourceGroup --environment MyContainerappEnv
- name: List patchable and non-patchable container apps.
text: |
az containerapp list -g MyResourceGroup --environment MyContainerappEnv --showAll
"""
23 changes: 23 additions & 0 deletions src/containerapp/azext_containerapp/_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -293,3 +293,26 @@
"validationMethod": None # str
}
}

# ContainerApp Patch
ImageProperties = {
"imageName": None,
"targetContainerAppName": None
}

ImagePatchableCheck = {
"targetContainerAppName": None,
"oldRunImage": None,
"newRunImage": None,
"id": None,
"reason": None
}

OryxMarinerRunImgTagProperty = {
"fullTag": None,
"framework": None,
"version": None,
"marinerVersion": None,
"architectures": None,
"support": None,
}
5 changes: 5 additions & 0 deletions src/containerapp/azext_containerapp/_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -414,3 +414,8 @@ def load_arguments(self, _):
c.argument('workload_profile_type', help="The type of workload profile to add or update. Run 'az containerapp env workload-profile list-supported -l <region>' to check the options for your region.")
c.argument('min_nodes', help="The minimum node count for the workload profile")
c.argument('max_nodes', help="The maximum node count for the workload profile")

with self.argument_context('containerapp patch list') as c:
c.argument('resource_group_name', options_list=['--rg','-g'], configured_default='resource_group_name', id_part=None)
c.argument('environment', options_list=['--environment'], help='Name or resource id of the Container App environment.')
c.argument('show_all', options_list=['--show-all'],help='Show all patchable and non-patchable containerapps')
75 changes: 75 additions & 0 deletions src/containerapp/azext_containerapp/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@
import time
import json
import platform
import hashlib
import requests
import packaging.version as SemVer
import re

from urllib.parse import urlparse
from datetime import datetime
Expand All @@ -31,6 +35,7 @@
LOG_ANALYTICS_RP, CONTAINER_APPS_RP, CHECK_CERTIFICATE_NAME_AVAILABILITY_TYPE, ACR_IMAGE_SUFFIX,
LOGS_STRING, PENDING_STATUS, SUCCEEDED_STATUS, UPDATING_STATUS)
from ._models import (ContainerAppCustomDomainEnvelope as ContainerAppCustomDomainEnvelopeModel, ManagedCertificateEnvelop as ManagedCertificateEnvelopModel)
from ._models import ImagePatchableCheck, OryxMarinerRunImgTagProperty

logger = get_logger(__name__)

Expand Down Expand Up @@ -1714,3 +1719,73 @@ def format_location(location=None):
if location:
return location.lower().replace(" ", "").replace("(", "").replace(")", "")
return location

def patchableCheck(repoTagSplit: str, oryxBuilderRunImgTags, bom):
tagProp = parseOryxMarinerTag(repoTagSplit)
repoTagSplit = repoTagSplit.split("-")
if repoTagSplit[1] == "dotnet":
matchingVersionInfo = oryxBuilderRunImgTags[repoTagSplit[2]][str(tagProp["version"].major) + "." + str(tagProp["version"].minor)][tagProp["support"]][tagProp["marinerVersion"]]

# Check if the image minor version is four less than the latest minor version
if tagProp["version"] < matchingVersionInfo[0]["version"]:
result = ImagePatchableCheck
result["targetContainerAppName"] = bom["targetContainerAppName"]
result["oldRunImage"] = tagProp["fullTag"]
if (tagProp["version"].minor == matchingVersionInfo[0]["version"].minor) and (tagProp["version"].micro < matchingVersionInfo[0]["version"].micro):
# Patchable
result["newRunImage"] = "mcr.microsoft.com/oryx/builder:" + matchingVersionInfo[0]["fullTag"]
result["id"] = hashlib.md5(str(result["oldRunImage"] + result["targetContainerAppName"] + result["newRunImage"]).encode()).hexdigest()
result["reason"] = "New security patch released for your current run image."
else:
# Not patchable
result["newRunImage"] = "mcr.microsoft.com/oryx/builder:" + matchingVersionInfo[0]["fullTag"]
result["reason"] = "The image is not pachable Please check for major or minor version upgrade."
else:
result = ImagePatchableCheck
result["targetContainerAppName"] = bom["targetContainerAppName"]
result["oldRunImage"] = tagProp["fullTag"]
result["reason"] = "You're already up to date!"
return result

def getCurrentMarinerTags() -> list(OryxMarinerRunImgTagProperty):
r = requests.get("https://mcr.microsoft.com/v2/oryx/builder/tags/list")
tags = r.json()
# tags = dict(tags=["run-dotnet-aspnet-7.0.1-cbl-mariner2.0", "run-dotnet-aspnet-7.0.1-cbl-mariner1.0", "run-dotnet-aspnet-7.1.0-cbl-mariner2.0"])
tagList = {}
# only keep entries that container keyword "mariner"
tags = [tag for tag in tags["tags"] if "mariner" in tag]
for tag in tags:
tagObj = parseOryxMarinerTag(tag)
if tagObj:
majorMinorVer = str(tagObj["version"].major) + "." + str(tagObj["version"].minor)
support = tagObj["support"]
framework = tagObj["framework"]
marinerVer = tagObj["marinerVersion"]
if framework in tagList.keys():
if majorMinorVer in tagList[framework].keys():
if support in tagList[framework][majorMinorVer].keys():
if marinerVer in tagList[framework][majorMinorVer][support].keys():
tagList[framework][majorMinorVer][support][marinerVer].append(tagObj)
tagList[framework][majorMinorVer][support][marinerVer].sort(reverse=True, key=lambda x: x["version"])
else:
tagList[framework][majorMinorVer][support][marinerVer] = [tagObj]
else:
tagList[framework][majorMinorVer][support] = {marinerVer: [tagObj]}
else:
tagList[framework][majorMinorVer] = {support: {marinerVer: [tagObj]}}
else:
tagList[framework] = {majorMinorVer: {support: {marinerVer: [tagObj]}}}
return tagList

def parseOryxMarinerTag(tag: str) -> OryxMarinerRunImgTagProperty:
tagSplit = tag.split("-")
if tagSplit[0] == "run" and tagSplit[1] == "dotnet":
versionRE = r"(\d+\.\d+(\.\d+)?).*?(cbl-mariner(\d+\.\d+))"
REmatches = re.findall(versionRE, tag)
if REmatches.count == 0:
tagObj = None
else:
tagObj = dict(fullTag=tag, version=SemVer.parse(REmatches[0][0]), framework=tagSplit[2], marinerVersion=REmatches[0][2], architectures=None, support="lts")
else:
tagObj = None
return tagObj
3 changes: 3 additions & 0 deletions src/containerapp/azext_containerapp/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -199,3 +199,6 @@ def load_command_table(self, _):
g.custom_show_command('show', 'show_workload_profile')
g.custom_command('set', 'set_workload_profile')
g.custom_command('delete', 'delete_workload_profile')

with self.command_group('containerapp patch', is_preview=True) as g:
g.custom_command('list', 'patch_list')
86 changes: 83 additions & 3 deletions src/containerapp/azext_containerapp/custom.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
import time
from urllib.parse import urlparse
import requests
import json
import subprocess

from azure.cli.core.azclierror import (
RequiredArgumentMissingError,
Expand Down Expand Up @@ -57,6 +59,7 @@
ScaleRule as ScaleRuleModel,
Volume as VolumeModel,
VolumeMount as VolumeMountModel,)
from ._models import OryxMarinerRunImgTagProperty, ImagePatchableCheck, ImageProperties

from ._utils import (_validate_subscription_registered, _ensure_location_allowed,
parse_secret_flags, store_as_secret_and_return_secret_ref, parse_env_var_flags,
Expand All @@ -73,7 +76,8 @@
validate_environment_location, safe_set, parse_metadata_flags, parse_auth_flags, _azure_monitor_quickstart,
set_ip_restrictions, certificate_location_matches, certificate_matches, generate_randomized_managed_cert_name,
check_managed_cert_name_availability, prepare_managed_certificate_envelop,
get_default_workload_profile_name_from_env, get_default_workload_profiles, ensure_workload_profile_supported, _generate_secret_volume_name)
get_default_workload_profile_name_from_env, get_default_workload_profiles, ensure_workload_profile_supported, _generate_secret_volume_name,
getCurrentMarinerTags, patchableCheck)
from ._validators import validate_create, validate_revision_suffix
from ._ssh_utils import (SSH_DEFAULT_ENCODING, WebSocketConnection, read_ssh, get_stdin_writer, SSH_CTRL_C_MSG,
SSH_BACKUP_ENCODING)
Expand Down Expand Up @@ -4129,8 +4133,7 @@ def show_auth_config(cmd, resource_group_name, name):
return auth_settings

# Compose



def create_containerapps_from_compose(cmd, # pylint: disable=R0914
resource_group_name,
managed_env,
Expand Down Expand Up @@ -4297,3 +4300,80 @@ def delete_workload_profile(cmd, resource_group_name, env_name, workload_profile
return r
except Exception as e:
handle_raw_exception(e)

def patch_list(cmd, resource_group_name, managed_env, show_all=False):
caList = list_containerapp(cmd, resource_group_name, managed_env)
imgs = []
if caList:
for ca in caList:
containers = ca["properties"]["template"]["containers"]
for container in containers:
result = dict(imageName=container["image"], targetContainerAppName=container["name"])
imgs.append(result)

# Get the BOM of the images
results = []
boms = []

## For production
#
for img in imgs:
subprocess.run("pack inspect-image " + img["imageName"] + " --output json > ./bom.json 2>&1", shell=True)
with open("./bom.json", "rb") as f:
lines = f.read()
bom = json.loads(lines)
bom.update({ "targetContainerAppName": img["targetContainerAppName"] })
boms.append(bom)

## For testing
#
# with open("./bom.json", "rb") as f:
# lines = f.read()
# # if lines.find(b"status code 401 Unauthorized") == -1 or lines.find(b"unable to find image") == -1:
# # bom = dict(remote_info=401)
# # else:
# bom = json.loads(lines)
# bom.update({ "targetContainerAppName": "test-containerapp-1" })
# boms.append(bom)

# Get the current tags of Dotnet Mariners
oryxRunImgTags = getCurrentMarinerTags()

# Start checking if the images are based on Mariner
for bom in boms:
if bom["remote_info"] == 401:
result = ImagePatchableCheck
result["targetContainerAppName"] = bom["targetContainerAppName"]
result["reason"] = "Failed to get BOM of the image. Please check if the image exists or you have the permission to access the image."
results.append(result)
else:
# devide run-images into different parts by "/"
runImagesProps = bom["remote_info"]["run_images"]
if runImagesProps is None:
result = ImagePatchableCheck
result["targetContainerAppName"] = bom["targetContainerAppName"]
result["reason"] = "Image not based on Mariners"
results.append(result)
else:
for runImagesProp in runImagesProps:
if (runImagesProp["name"].find("mcr.microsoft.com/oryx/builder") != -1):
runImagesProp = runImagesProp["name"].split(":")
runImagesTag = runImagesProp[1]
# Based on Mariners
if runImagesTag.find('mariner') != -1:
result = patchableCheck(runImagesTag, oryxRunImgTags, bom=bom)
else:
result = ImagePatchableCheck
result["targetContainerAppName"] = bom["targetContainerAppName"]
result["oldRunImage"] = bom["remote_info"]["run_images"]
result["reason"] = "Image not based on Mariners"
else:
# Not based on image from mcr.microsoft.com/dotnet
result = ImagePatchableCheck
result["targetContainerAppName"] = bom["targetContainerAppName"]
result["oldRunImage"] = bom["remote_info"]["run_images"]
result["reason"] = "Image not from mcr.microsoft.com/oryx/builder"
results.append(result)
if show_all == False :
results = [x for x in results if x["newRunImage"] != None]
return results

0 comments on commit 2b5cb59

Please sign in to comment.