Skip to content

Commit d2b4a57

Browse files
committed
WIP: Template upstream and downstream CSV manifests
1 parent cdcde60 commit d2b4a57

7 files changed

Lines changed: 493 additions & 0 deletions

Makefile

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,3 +91,34 @@ CODEGEN_IMAGE = container-security-operator:codegen
9191
.PHONY: BUILD_CODEGEN_IMAGE
9292
BUILD_CODEGEN_IMAGE:
9393
docker build -f Dockerfile.codegen -t $(CODEGEN_IMAGE) .
94+
95+
96+
# =======================
97+
# CSV Manifest generation
98+
# =======================
99+
MANIFESTGEN_IMAGE = container-security-operator:manifestgen
100+
101+
MANIFESTGEN_WORKDIR ?= scripts
102+
MANIFESTGEN_OUTPUT_DIR ?= deploy
103+
MANIFESTGEN_VERSION ?= master
104+
MANIFESTGEN_OPT_FLAGS ?= --upstream --skip-pull --yaml
105+
106+
OPERATOR_IMAGE ?= quay.io/quay/container-security-operator
107+
OPERATOR_IMAGE_REF ?= $(shell \
108+
docker pull $(OPERATOR_IMAGE):$(MANIFESTGEN_VERSION) > /dev/null && \
109+
docker inspect $(OPERATOR_IMAGE):$(MANIFESTGEN_VERSION) | jq '.[0].RepoDigests[] | select(. | startswith("$(OPERATOR_IMAGE)"))' \
110+
)
111+
112+
.PHONY: BUILD_MANIFESTGEN_IMAGE
113+
BUILD_MANIFEST_GEN_IMAGE:
114+
docker build -t $(MANIFESTGEN_IMAGE) scripts
115+
116+
.PHONY: manifestgen-container
117+
manifestgen-container: BUILD_MANIFEST_GEN_IMAGE
118+
docker run --rm --name manifestgen \
119+
-v $(PWD)/$(MANIFESTGEN_WORKDIR):/workspace/$(MANIFESTGEN_WORKDIR) $(MANIFESTGEN_PREVIOUS_VERSION) \
120+
-v $(PWD)/$(MANIFESTGEN_OUTPUT_DIR):/workspace/$(MANIFESTGEN_OUTPUT_DIR) \
121+
$(MANIFESTGEN_IMAGE) \
122+
python $(MANIFESTGEN_WORKDIR)/generate_csv.py $(MANIFESTGEN_VERSION) \
123+
--workdir $(MANIFESTGEN_WORKDIR) --output-dir $(MANIFESTGEN_OUTPUT_DIR) \
124+
--image $(OPERATOR_IMAGE_REF) $(MANIFEST_GEN_OPT_FLAGS)

scripts/Dockerfile

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
FROM python:3
2+
3+
WORKDIR /workspace
4+
RUN pip install jinja2 pyyaml
5+
6+
CMD ["python", "--version"]

scripts/generate_csv.py

Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
import argparse
2+
import base64
3+
import logging
4+
import json
5+
import os
6+
import re
7+
import subprocess
8+
import sys
9+
import yaml
10+
11+
from urllib.parse import urljoin
12+
13+
from jinja2 import FileSystemLoader, Environment, StrictUndefined
14+
15+
16+
logger = logging.getLogger(__name__)
17+
18+
LOGO_DOWNSTREAM_FILE = "img/downstream_logo.png"
19+
LOGO_UPSTREAM_FILE = "img/upstream_logo.png"
20+
21+
PACKAGE_NAME = "container-security-operator"
22+
23+
# Default location for the image
24+
REGISTRY_HOST = "quay.io"
25+
REGISTRY_API_BASE = REGISTRY_HOST + "/api/v1/"
26+
27+
CSO_REPO = "projectquay/" + PACKAGE_NAME
28+
CSO_IMAGE = REGISTRY_HOST + "/" + CSO_REPO
29+
CSO_IMAGE_TAG = "master"
30+
31+
CSO_CATALOG_REPO = "projectquay/cso-catalog"
32+
CSO_CATALOG_IMAGE = REGISTRY_HOST + "/" + CSO_CATALOG_REPO
33+
CSO_CATALOG_IMAGE_TAG = "master"
34+
35+
# Default template values
36+
K8S_API_VERSION = "v1alpha1"
37+
38+
# Jinja templates
39+
TEMPLATE_DIR = "templates"
40+
CSV_TEMPLATE_FILE = PACKAGE_NAME + ".clusterserviceversion.yaml.jnj"
41+
CRD_TEMPLATE_FILES = [
42+
"imagemanifestvulns.secscan.quay.redhat.com.crd.yaml.jnj"
43+
]
44+
45+
# Output
46+
OUTPUT_MANIFEST_DIR = os.path.join("manifests", PACKAGE_NAME)
47+
OUTPUT_CATALOG_FILE = "cso.catalogsource.yaml"
48+
49+
MANIFEST_DIGEST_REGEX = re.compile(r"sha256:[a-z0-9]{64}")
50+
ARGUMENT_REGEX = re.compile(r"(-[\w])|(--[\w]+)")
51+
VERSION_REGEX = re.compile(r"^v(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(-(0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(\.(0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*)?(\+[0-9a-zA-Z-]+(\.[0-9a-zA-Z-]+)*)?$")
52+
MASTER_VERSION_REGEX = re.compile(r"^master$")
53+
54+
55+
def normalize_version(version):
56+
if VERSION_REGEX.match(version):
57+
return version[1:]
58+
return version
59+
60+
61+
def get_image_manifest_digest(image_ref, cmd="docker"):
62+
""" Return the repo and manifest digest of a given image reference.
63+
e.g quay.io/namespace/repo:tag -> (quay.io/namespace/repo, sha256:123456)
64+
"""
65+
if len(image_ref.split("@")) == 2:
66+
# Still pull for a digest ref, to make sure the image exists
67+
repo, digest = image_ref.split("@")
68+
pull_command = [cmd, "pull", repo+"@"+tag]
69+
inspect_command = [cmd, "inspect", repo+"@"+tag]
70+
else:
71+
repo, tag = image_ref.split(":")
72+
pull_command = [cmd, "pull", repo+":"+tag]
73+
inspect_command = [cmd, "inspect", repo+":"+tag]
74+
75+
try:
76+
subprocess.run(pull_command, check=True)
77+
out = subprocess.run(inspect_command, check=True, capture_output=True)
78+
parsed = json.loads(out.stdout)
79+
repo_digests = parsed[0]["RepoDigests"]
80+
except subprocess.CalledProcessError as cpe:
81+
logger.error("Error running docker commands for image %s:%s - %s", repo, tag, cpe)
82+
return None, None
83+
except ValueError as ve:
84+
logger.error("Error parsing docker inspect output output - %s", ve)
85+
return None, None
86+
except Exception as e:
87+
logger.error("Error getting the manifest digest for image %s:%s - %s", repo, tag, e)
88+
return None, None
89+
90+
repo_digests = list(filter(lambda repo_digest: repo_digest.startswith(repo),repo_digests))
91+
if len(repo_digests) == 0:
92+
logger.error("Could not find the manifest digest for the given image %s:%s", repo, tag)
93+
return None, None
94+
95+
manifest_digest = repo_digests[0].split("@")[-1]
96+
if not MANIFEST_DIGEST_REGEX.match(manifest_digest):
97+
logger.error("Unknown manifest digest format for %s:%s -> %s", repo_digest, manifest_digest)
98+
return None, None
99+
100+
return repo, manifest_digest
101+
102+
103+
def get_b64_logo_from_file(filepath):
104+
with open(filepath, 'rb') as f:
105+
data = f.read()
106+
107+
return base64.b64encode(data).decode("ascii")
108+
109+
110+
def parse_args():
111+
def version_arg_type(arg_value, pat=re.compile(VERSION_REGEX)):
112+
if not pat.match(arg_value):
113+
if MASTER_VERSION_REGEX.match(arg_value):
114+
return arg_value
115+
116+
if not pat.match("v"+arg_value):
117+
raise argparse.ArgumentTypeError
118+
119+
return "v"+arg_value
120+
return arg_value
121+
122+
desc = 'Generate CSVs for tagged versions.'
123+
parser = argparse.ArgumentParser(description=desc)
124+
parser.add_argument('version', help='Version to generate (SemVer). e.g v1.2.3', type=version_arg_type)
125+
parser.add_argument('previous_version', help='Previous version.', type=version_arg_type, nargs='?')
126+
parser.add_argument('--json', dest='yaml', help='Output json config (default).', action='store_false')
127+
parser.add_argument('--yaml', dest='yaml', help='Output yaml config.', action='store_true')
128+
parser.add_argument('--upstream', dest='downstream', help='Generate with upstream config.', action='store_false')
129+
parser.add_argument('--downstream', dest='downstream', help='Generate with downstream config.', action='store_true')
130+
parser.add_argument('--image', dest='image', help='Image to use in CSV.')
131+
parser.add_argument('--workdir', dest='workdir', help='Work directory', default=".")
132+
parser.add_argument('--output-dir', dest='output_dir', help='Output directory relative to the workdir', default="deploy")
133+
parser.add_argument('--skip-pull', dest='skip_pull', help='Skip pulling the image for verification', action='store_true')
134+
parser.set_defaults(yaml=True)
135+
parser.set_defaults(downstream=False)
136+
parser.set_defaults(previous_version=None)
137+
parser.set_defaults(skip_pull=False)
138+
139+
logger.debug('Parsing all args')
140+
_, unknown = parser.parse_known_args()
141+
142+
added_args_keys = set()
143+
while (len(unknown) > 0 and ARGUMENT_REGEX.match(unknown[0]) and
144+
ARGUMENT_REGEX.match(unknown[0]).end() == len(unknown[0])):
145+
logger.info('Adding argument: %s', unknown[0])
146+
added_args_keys.add(unknown[0].lstrip('-'))
147+
parser.add_argument(unknown[0])
148+
_, unknown = parser.parse_known_args()
149+
150+
logger.debug('Parsing final set of args')
151+
return parser.parse_args(), added_args_keys
152+
153+
154+
def main():
155+
all_args, added_args_keys = parse_args()
156+
template_kwargs = {key: getattr(all_args, key, None) for key in added_args_keys}
157+
158+
ENV = Environment(loader=FileSystemLoader(os.path.join(all_args.workdir, TEMPLATE_DIR)), undefined=StrictUndefined)
159+
ENV.filters['normalize_version'] = normalize_version
160+
161+
logo = get_b64_logo_from_file(LOGO_DOWNSTREAM_FILE) if all_args.downstream else get_b64_logo_from_file(os.path.join(all_args.workdir,LOGO_UPSTREAM_FILE))
162+
image_ref = all_args.image or CSO_IMAGE + ":" + CSO_IMAGE_TAG
163+
164+
if not all_args.skip_pull:
165+
repo, image_manifest_digest = get_image_manifest_digest(image_ref)
166+
if not repo or not image_manifest_digest:
167+
sys.exit(1)
168+
169+
container_image = repo + "@" + image_manifest_digest
170+
else:
171+
container_image = image_ref
172+
173+
template_kwargs["version"] = all_args.version
174+
template_kwargs["previous_version"] = all_args.previous_version
175+
template_kwargs["logo"] = logo
176+
template_kwargs["container_image"] = container_image
177+
template_kwargs["k8s_api_version"] = template_kwargs.setdefault("k8s_api_version", K8S_API_VERSION)
178+
179+
manifest_output_dir = os.path.join(all_args.output_dir, OUTPUT_MANIFEST_DIR, normalize_version(all_args.version))
180+
os.makedirs(manifest_output_dir, exist_ok=True)
181+
generated_files = {}
182+
183+
assert CSV_TEMPLATE_FILE.endswith(".clusterserviceversion.yaml.jnj")
184+
csv_template = ENV.get_template(CSV_TEMPLATE_FILE)
185+
generated_csv = csv_template.render(**template_kwargs)
186+
csv_filename = CSV_TEMPLATE_FILE.split(".")
187+
csv_filename.insert(1, all_args.version)
188+
csv_filename = ".".join(csv_filename[:-1])
189+
generated_files[os.path.join(manifest_output_dir, csv_filename)] = generated_csv
190+
191+
for crd_template_file in filter(lambda filename: filename.endswith(".crd.yaml.jnj"), CRD_TEMPLATE_FILES):
192+
crd_template = ENV.get_template(crd_template_file)
193+
generated_crd = crd_template.render(**template_kwargs)
194+
generated_files[os.path.join(manifest_output_dir, crd_template_file.rstrip(".jnj"))] = generated_crd
195+
196+
if all_args.yaml:
197+
for filepath, content in generated_files.items():
198+
with open(filepath, 'w') as f:
199+
f.write(content)
200+
else:
201+
for filepath, content in generated_files.items():
202+
parsed = yaml.load(content, Loader=yaml.SafeLoader)
203+
with open(filepath.rstrip("yaml")+"json", 'w') as f:
204+
f.write(json.dumps(parsed, default=str))
205+
206+
207+
if __name__ == "__main__":
208+
main()

scripts/img/downstream_logo.png

3.47 KB
Loading

scripts/img/upstream_logo.png

6.94 KB
Loading
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
apiVersion: operators.coreos.com/{{ k8s_api_version }}
2+
kind: ClusterServiceVersion
3+
metadata:
4+
annotations:
5+
capabilities: Full Lifecycle
6+
categories: Security
7+
containerImage: {{ container_image }}
8+
createdAt: 2019-11-16 01:03:00
9+
description: Identify image vulnerabilities in Kubernetes pods
10+
repository: https://github.com/quay/container-security-operator
11+
tectonic-visibility: ocs
12+
name: container-security-operator.{{ version }}
13+
namespace: placeholder
14+
spec:
15+
customresourcedefinitions:
16+
owned:
17+
- description: Represents a set of vulnerabilities in an image manifest.
18+
displayName: Image Manifest Vulnerability
19+
kind: ImageManifestVuln
20+
name: imagemanifestvulns.secscan.quay.redhat.com
21+
version: {{ k8s_api_version }}
22+
description: "The Container Security Operator (CSO) brings Quay and Clair metadata to Kubernetes / OpenShift.\
23+
\ Starting with vulnerability information the scope will get expanded over time. If it runs on OpenShift,\
24+
\ the corresponding vulnerability information is shown inside the OCP Console. The Container Security Operator\
25+
\ enables cluster administrators to monitor known container\
26+
\ image vulnerabilities in pods running on their Kubernetes cluster. The controller sets up a watch\
27+
\ on pods in the specified namespace(s) and queries the container registry for vulnerability\
28+
\ information. If the container registry supports image scanning,\
29+
\ such as [Quay](https://github.com/quay/quay) with [Clair](https://github.com/quay/clair),\
30+
\ then the Operator will expose any vulnerabilities found via the Kubernetes API in an\
31+
\ `ImageManifestVuln` object. This Operator requires no additional configuration after deployment,\
32+
\ and will begin watching pods and populating `ImageManifestVulns` immediately once installed."
33+
displayName: Container Security
34+
install:
35+
spec:
36+
deployments:
37+
- name: container-security-operator
38+
spec:
39+
replicas: 1
40+
selector:
41+
matchLabels:
42+
name: container-security-operator-alm-owned
43+
template:
44+
metadata:
45+
labels:
46+
name: container-security-operator-alm-owned
47+
name: container-security-operator-alm-owned
48+
spec:
49+
containers:
50+
- command:
51+
- /bin/security-labeller
52+
- '--namespaces=$(WATCH_NAMESPACE)'
53+
env:
54+
- name: MY_POD_NAMESPACE
55+
valueFrom:
56+
fieldRef:
57+
fieldPath: metadata.namespace
58+
- name: MY_POD_NAME
59+
valueFrom:
60+
fieldRef:
61+
fieldPath: metadata.name
62+
- name: WATCH_NAMESPACE
63+
valueFrom:
64+
fieldRef:
65+
fieldPath: metadata.annotations['olm.targetNamespaces']
66+
image: {{ container_image }}
67+
name: container-security-operator
68+
serviceAccountName: container-security-operator
69+
permissions:
70+
- rules:
71+
- apiGroups:
72+
- secscan.quay.redhat.com
73+
resources:
74+
- imagemanifestvulns
75+
- imagemanifestvulns/status
76+
verbs:
77+
- '*'
78+
- apiGroups:
79+
- ''
80+
resources:
81+
- pods
82+
- events
83+
verbs:
84+
- '*'
85+
- apiGroups:
86+
- ''
87+
resources:
88+
- secrets
89+
verbs:
90+
- get
91+
serviceAccountName: container-security-operator
92+
strategy: deployment
93+
installModes:
94+
- supported: true
95+
type: OwnNamespace
96+
- supported: true
97+
type: SingleNamespace
98+
- supported: true
99+
type: MultiNamespace
100+
- supported: true
101+
type: AllNamespaces
102+
keywords:
103+
- open source
104+
- containers
105+
- security
106+
labels:
107+
alm-owner-container-security-operator: container-security-operator
108+
operated-by: container-security-operator
109+
icon:
110+
- base64data: {{ logo }}
111+
mediatype: image/png
112+
maturity: alpha
113+
links:
114+
- name: Source Code
115+
url: https://github.com/quay/container-security-operator
116+
maintainers:
117+
- email: quay-devel@redhat.com
118+
name: Quay Engineering Team
119+
provider:
120+
name: Red Hat
121+
selector:
122+
matchLabels:
123+
alm-owner-container-security-operator: container-security-operator
124+
operated-by: container-security-operator
125+
version: {{ version | normalize_version }}
126+
{% if previous_version %}
127+
replaces: {{ previous_version }}
128+
{% endif %}

0 commit comments

Comments
 (0)