Skip to content

Commit b460191

Browse files
committed
Template upstream and downstream CSV manifests
1 parent 6f80f04 commit b460191

7 files changed

+514
-0
lines changed

Makefile

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,3 +91,37 @@ 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) \
120+
-v $(PWD)/$(MANIFESTGEN_OUTPUT_DIR):/workspace/$(MANIFESTGEN_OUTPUT_DIR) \
121+
$(MANIFESTGEN_IMAGE) \
122+
python $(MANIFESTGEN_WORKDIR)/generate_csv.py $(MANIFESTGEN_VERSION) $(MANIFESTGEN_PREVIOUS_VERSION) \
123+
--workdir $(MANIFESTGEN_WORKDIR) --output-dir $(MANIFESTGEN_OUTPUT_DIR) \
124+
--image $(OPERATOR_IMAGE_REF) $(MANIFESTGEN_OPT_FLAGS)
125+
126+
# Example:
127+
# $ OPERATOR_IMAGE_REF=quay.io/quay/container-security-operator:v1.0.0 MANIFESTGEN_OUTPUT_DIR=testingscript MANIFESTGEN_VERSION=v3.3.0 make manifestgen-container

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

scripts/img/downstream_logo.png

3.47 KB
Loading

scripts/img/upstream_logo.png

6.94 KB
Loading
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
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: {{ get_current_datetime() }}
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+
- '--extraCerts=/extra-certs'
54+
env:
55+
- name: MY_POD_NAMESPACE
56+
valueFrom:
57+
fieldRef:
58+
fieldPath: metadata.namespace
59+
- name: MY_POD_NAME
60+
valueFrom:
61+
fieldRef:
62+
fieldPath: metadata.name
63+
- name: WATCH_NAMESPACE
64+
valueFrom:
65+
fieldRef:
66+
fieldPath: metadata.annotations['olm.targetNamespaces']
67+
image: {{ container_image }}
68+
volumeMounts:
69+
- name: extra-certs
70+
readOnly: true
71+
mountPath: /extra-certs
72+
name: container-security-operator
73+
serviceAccountName: container-security-operator
74+
volumes:
75+
- name: extra-certs
76+
secret:
77+
optional: true
78+
secretName: container-security-operator-extra-certs
79+
permissions:
80+
- rules:
81+
- apiGroups:
82+
- secscan.quay.redhat.com
83+
resources:
84+
- imagemanifestvulns
85+
- imagemanifestvulns/status
86+
verbs:
87+
- '*'
88+
- apiGroups:
89+
- ''
90+
resources:
91+
- pods
92+
- events
93+
verbs:
94+
- '*'
95+
- apiGroups:
96+
- ''
97+
resources:
98+
- secrets
99+
verbs:
100+
- get
101+
serviceAccountName: container-security-operator
102+
strategy: deployment
103+
installModes:
104+
- supported: true
105+
type: OwnNamespace
106+
- supported: true
107+
type: SingleNamespace
108+
- supported: true
109+
type: MultiNamespace
110+
- supported: true
111+
type: AllNamespaces
112+
keywords:
113+
- open source
114+
- containers
115+
- security
116+
labels:
117+
alm-owner-container-security-operator: container-security-operator
118+
operated-by: container-security-operator
119+
icon:
120+
- base64data: {{ logo }}
121+
mediatype: image/png
122+
maturity: alpha
123+
links:
124+
- name: Source Code
125+
url: https://github.com/quay/container-security-operator
126+
maintainers:
127+
128+
name: Quay Engineering Team
129+
provider:
130+
name: Red Hat
131+
selector:
132+
matchLabels:
133+
alm-owner-container-security-operator: container-security-operator
134+
operated-by: container-security-operator
135+
version: {{ version | normalize_version }}
136+
{% if previous_version %}replaces: container-security-operator.{{ previous_version }}
137+
{% endif %}

0 commit comments

Comments
 (0)