blob: 12925a0c10f851f8d464673b17b73ce3b8ebfdfd [file]
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
"""Private application of datastore credentials and narrow ingress policies."""
import base64
import json
import subprocess
import openserverless.kube as kube
def apply_private(spec, namespace='openserverless'):
# The generic wrapper includes stdin and stderr in error logs.
result = subprocess.run(['kubectl', '-n', namespace, 'apply', '-f', '-'],
input=json.dumps(spec), text=True, capture_output=True,
timeout=60)
if result.returncode:
raise RuntimeError('Could not apply private datastore resources')
def apply(spec):
items = spec.get('items', [spec])
private = [item for item in items if item['kind'] == 'Secret']
public = [item for item in items if item['kind'] != 'Secret']
if private:
apply_private({'apiVersion': 'v1', 'kind': 'List', 'items': private})
return kube.apply({'apiVersion': 'v1', 'kind': 'List', 'items': public})
def ensure_jwt_key(owner=None):
"""Keep a per-installation key; never reuse the legacy repository key."""
name = 'openserverless-etcd-jwt-token-v2'
raw = kube.kubectl('get', 'secret', name, '--ignore-not-found', '-o', 'json',
debugresult=False)
if raw.strip():
key = base64.b64decode(json.loads(raw)['data']['jwt-token.pem'], validate=True)
check = subprocess.run(['openssl', 'rsa', '-check', '-noout'], input=key,
capture_output=True, timeout=30)
if check.returncode:
raise RuntimeError('Invalid etcd signing key; refusing implicit rotation')
return name
# A missing key on an already patched cluster is a security state error.
current = kube.kubectl('get', 'sts', 'openserverless-etcd', '--ignore-not-found',
'-o', 'json', debugresult=False)
if current.strip():
volumes = json.loads(current)['spec']['template']['spec'].get('volumes', [])
if any(v.get('secret', {}).get('secretName') == name for v in volumes):
raise RuntimeError('Missing etcd signing key; refusing implicit rotation')
result = subprocess.run(['openssl', 'genpkey', '-algorithm', 'RSA',
'-pkeyopt', 'rsa_keygen_bits:3072'],
capture_output=True, timeout=30)
if result.returncode:
raise RuntimeError('Could not generate etcd signing key')
resource = {'apiVersion': 'v1', 'kind': 'Secret', 'type': 'Opaque',
'metadata': {'name': name, 'namespace': 'openserverless'},
'data': {'jwt-token.pem': base64.b64encode(result.stdout).decode()}}
if owner:
import kopf
kopf.append_owner_reference([resource], owner)
apply_private(resource)
return name
def policy(component):
if component == 'etcd':
selector = {'app.kubernetes.io/name': 'openserverless-etcd'}
ingress = [
{'from': [{'podSelector': {'matchLabels': selector}}],
'ports': [{'port': 2379, 'protocol': 'TCP'}, {'port': 2380, 'protocol': 'TCP'}]},
{'from': [{'podSelector': {'matchLabels': {'name': 'openserverless-operator'}}},
{'podSelector': {'matchLabels': {'name': 'openserverless-milvus'}}}],
'ports': [{'port': 2379, 'protocol': 'TCP'}]}]
elif component == 'milvus':
selector = {'name': 'openserverless-milvus'}
ingress = [
{'ports': [{'port': 19530, 'protocol': 'TCP'}]},
{'from': [{'podSelector': {'matchLabels': {
'app.kubernetes.io/name': 'prometheus',
'app.kubernetes.io/instance': 'openserverless-prometheus'}}},
{'podSelector': {'matchLabels': {'name': 'openserverless-operator'}}}],
'ports': [{'port': 9091, 'protocol': 'TCP'}]}]
else:
raise ValueError('Unknown datastore component')
return {'apiVersion': 'networking.k8s.io/v1', 'kind': 'NetworkPolicy',
'metadata': {'name': 'openserverless-'+component+'-isolation', 'namespace': 'openserverless'},
'spec': {'podSelector': {'matchLabels': selector},
'policyTypes': ['Ingress'], 'ingress': ingress}}
def finish_milvus_reconcile():
kube.kubectl('rollout', 'status', 'deployment/openserverless-milvus-standalone', '--timeout=300s')
# Remove the old ConfigMap AND its last-applied annotation by deleting it.
# Its contents include S3 and Milvus admin credentials as well as etcd root.
kube.kubectl('delete', 'configmap', 'openserverless-milvus', '--ignore-not-found')