Skip to content

Commit 3b46c7a

Browse files
authored
Merge pull request #34 from zakkg3/0.8
0.8
2 parents 30c12bb + 5b7316e commit 3b46c7a

File tree

8 files changed

+179
-106
lines changed

8 files changed

+179
-106
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
src/__pycache__/
2+
yaml/Object_example/debug-*

Makefile

+5-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
IMG_NAMESPACE = flag5
22
IMG_NAME = clustersecret
33
IMG_FQNAME = $(IMG_NAMESPACE)/$(IMG_NAME)
4-
IMG_VERSION = 0.0.7
4+
IMG_VERSION = 0.0.8
55

66
.PHONY: container push clean arm-container arm-push arm-clean
77
all: container push
@@ -27,3 +27,7 @@ arm-push: arm-container
2727

2828
arm-clean:
2929
sudo docker rmi $(IMG_FQNAME):$(IMG_VERSION)_arm32
30+
31+
beta:
32+
sudo docker build -t $(IMG_FQNAME):$(IMG_VERSION)-beta .
33+
sudo docker push $(IMG_FQNAME):$(IMG_VERSION)-beta

README.md

+21-3
Original file line numberDiff line numberDiff line change
@@ -112,11 +112,30 @@ data:
112112
113113
This can be archived by changing the RBAC.
114114
You may want to replace https://github.com/zakkg3/ClusterSecret/blob/master/yaml/00_rbac.yaml#L43-L46
115-
for a new namespaced role and its correspondient rolebinding.
115+
for a new namespaced role and its correspondent rolebinding.
116116
117117
Here is the official doc:
118118
https://kubernetes.io/docs/reference/access-authn-authz/rbac/
119119
120+
## Update a ClusterSecret object
121+
122+
This will trigger the operator to also update all secrets that it matches.
123+
124+
## Value From another secret.
125+
126+
With this we can tell ClusterSecret to take the values from an existing secret.
127+
yaml/Object_example/value-from-obj.yaml have a working example. Note that you will need first to have the obj2.yaml applied (the source secret).
128+
129+
```
130+
data:
131+
valueFrom:
132+
secretKeyRef:
133+
name: <secre-name>
134+
namespace: <source-namespace>
135+
```
136+
137+
to-do is to specify keys or matched keys to only sync that ones. For now it will sync the whole secret.
138+
120139
## optional
121140
122141
overwrite the deployment command with kopf namespaces instead of the "-A" (all namespaces)
@@ -175,8 +194,7 @@ docker.io/flag5/clustersecret:
175194
# Roadmap:
176195
177196
Tag 0.0.8:
178-
179-
- [] implement `source` to specify a source secret to sync instead of `data` field. (https://github.com/zakkg3/ClusterSecret/issues/3)
197+
- [X] implement `source` to specify a source secret to sync instead of `data` field. (https://github.com/zakkg3/ClusterSecret/issues/3)
180198
181199
182200

src/csHelper.py

+118
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import kopf
2+
import re
3+
from kubernetes import client
4+
5+
def get_ns_list(logger,body,v1=None):
6+
"""Returns a list of namespaces where the secret should be matched
7+
"""
8+
if v1 is None:
9+
v1 = client.CoreV1Api()
10+
logger.debug('new client - fn get_ns_list')
11+
12+
try:
13+
matchNamespace = body.get('matchNamespace')
14+
except KeyError:
15+
matchNamespace = '*'
16+
logger.debug("matching all namespaces.")
17+
logger.debug(f'Matching namespaces: {matchNamespace}')
18+
19+
if matchNamespace is None: # if delted key (issue 26)
20+
matchNamespace = '*'
21+
22+
try:
23+
avoidNamespaces = body.get('avoidNamespaces')
24+
except KeyError:
25+
avoidNamespaces = ''
26+
logger.debug("not avoiding namespaces")
27+
28+
nss = v1.list_namespace().items
29+
matchedns = []
30+
avoidedns = []
31+
32+
for matchns in matchNamespace:
33+
for ns in nss:
34+
if re.match(matchns, ns.metadata.name):
35+
matchedns.append(ns.metadata.name)
36+
logger.debug(f'Matched namespaces: {ns.metadata.name} matchpathern: {matchns}')
37+
if avoidNamespaces:
38+
for avoidns in avoidNamespaces:
39+
for ns in nss:
40+
if re.match(avoidns, ns.metadata.name):
41+
avoidedns.append(ns.metadata.name)
42+
logger.debug(f'Skipping namespaces: {ns.metadata.name} avoidpatrn: {avoidns}')
43+
# purge
44+
for ns in matchedns.copy():
45+
if ns in avoidedns:
46+
matchedns.remove(ns)
47+
48+
return matchedns
49+
50+
def read_data_secret(logger,name,namespace,v1):
51+
"""Gets the data from the 'name' secret in namspace
52+
"""
53+
data={}
54+
logger.debug(f'Reading {name} from ns {namespace}')
55+
try:
56+
secret = v1.read_namespaced_secret(name, namespace)
57+
logger.debug(f'Obtained secret {secret}')
58+
data = secret.data
59+
except client.exceptions.ApiException as e:
60+
logger.error(f'Error reading secret {e}')
61+
if e == "404":
62+
logger.error(f"Secret {name} in ns {namespace} not found!")
63+
raise kopf.TemporaryError("Error reading secret")
64+
return data
65+
66+
def create_secret(logger,namespace,body,v1=None):
67+
"""Creates a given secret on a given namespace
68+
"""
69+
if v1 is None:
70+
v1 = client.CoreV1Api()
71+
logger.debug('new client - fn create secret')
72+
try:
73+
sec_name = body['metadata']['name']
74+
except KeyError:
75+
logger.debug("No name in body ?")
76+
raise kopf.TemporaryError("can not get the name.")
77+
try:
78+
data = body.get('data')
79+
except KeyError:
80+
data = ''
81+
logger.error("Empty secret?? could not get the data.")
82+
83+
if 'valueFrom' in data:
84+
if len(data.keys()) > 1:
85+
logger.error(f'Data keys with ValueFrom error: {data.keys()} len {len(data.keys())}')
86+
raise kopf.TemporaryError("ValueFrom can not coexist with other keys in the data")
87+
88+
try:
89+
ns_from = data['valueFrom']['secretKeyRef']['namespace']
90+
name_from = data['valueFrom']['secretKeyRef']['name']
91+
# key_from = data['ValueFrom']['secretKeyRef']['name']
92+
# to-do specifie keys. for now. it will clone all.
93+
logger.debug(f'Taking value from secret {name_from} from namespace {ns_from} - All keys')
94+
data = read_data_secret(logger,name_from,ns_from,v1)
95+
except KeyError:
96+
logger.error (f'ERROR reading data from remote secret = {data}')
97+
raise kopf.TemporaryError("Can not get Values from external secret")
98+
99+
logger.debug(f'Going to create with data: {data}')
100+
secret_type = 'Opaque'
101+
if 'type' in body:
102+
secret_type = body['type']
103+
body = client.V1Secret()
104+
body.metadata = client.V1ObjectMeta(name=sec_name)
105+
body.type = secret_type
106+
body.data = data
107+
# kopf.adopt(body)
108+
logger.info(f"cloning secret in namespace {namespace}")
109+
try:
110+
api_response = v1.create_namespaced_secret(namespace, body)
111+
except client.rest.ApiException as e:
112+
if e.reason == 'Conflict':
113+
logger.info(f"secret `{sec_name}` already exist in namesace '{namespace}'")
114+
return 0
115+
logger.error(f'Can not create a secret, it is base64 encoded? data: {data}')
116+
logger.error(f'Kube exception {e}')
117+
return 1
118+
return 0

src/handlers.py

+10-101
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import kopf
2-
import re
32
from kubernetes import client, config
3+
from csHelper import *
44

5+
csecs = {} # all cluster secrets.
56

67
@kopf.on.delete('clustersecret.io', 'v1', 'clustersecrets')
78
def on_delete(spec,uid,body,name,logger=None, **_):
@@ -31,7 +32,14 @@ def on_delete(spec,uid,body,name,logger=None, **_):
3132
def on_field_data(old, new, body,name,logger=None, **_):
3233
logger.debug(f'Data changed: {old} -> {new}')
3334
if old is not None:
34-
syncedns = body['status']['create_fn']['syncedns']
35+
logger.debug(f'Updating Object body == {body}')
36+
37+
try:
38+
syncedns = body['status']['create_fn']['syncedns']
39+
except KeyError:
40+
logger.error('No Synced or status Namespaces found')
41+
syncedns=[]
42+
3543
v1 = client.CoreV1Api()
3644

3745
secret_type = 'Opaque'
@@ -56,8 +64,6 @@ def on_field_data(old, new, body,name,logger=None, **_):
5664
else:
5765
logger.debug('This is a new object')
5866

59-
csecs = {} # all cluster secrets.
60-
6167
@kopf.on.resume('clustersecret.io', 'v1', 'clustersecrets')
6268
@kopf.on.create('clustersecret.io', 'v1', 'clustersecrets')
6369
async def create_fn(spec,uid,logger=None,body=None,**kwargs):
@@ -78,103 +84,6 @@ async def create_fn(spec,uid,logger=None,body=None,**kwargs):
7884

7985
return {'syncedns': matchedns}
8086

81-
def get_ns_list(logger,body,v1=None):
82-
"""Returns a list of namespaces where the secret should be matched
83-
"""
84-
if v1 is None:
85-
v1 = client.CoreV1Api()
86-
logger.debug('new client - fn get_ns_list')
87-
88-
try:
89-
matchNamespace = body.get('matchNamespace')
90-
except KeyError:
91-
matchNamespace = '*'
92-
logger.debug("matching all namespaces.")
93-
logger.debug(f'Matching namespaces: {matchNamespace}')
94-
95-
if matchNamespace is None: # if delted key (issue 26)
96-
matchNamespace = '*'
97-
98-
try:
99-
avoidNamespaces = body.get('avoidNamespaces')
100-
except KeyError:
101-
avoidNamespaces = ''
102-
logger.debug("not avoiding namespaces")
103-
104-
nss = v1.list_namespace().items
105-
matchedns = []
106-
avoidedns = []
107-
108-
for matchns in matchNamespace:
109-
for ns in nss:
110-
if re.match(matchns, ns.metadata.name):
111-
matchedns.append(ns.metadata.name)
112-
logger.debug(f'Matched namespaces: {ns.metadata.name} matchpathern: {matchns}')
113-
if avoidNamespaces:
114-
for avoidns in avoidNamespaces:
115-
for ns in nss:
116-
if re.match(avoidns, ns.metadata.name):
117-
avoidedns.append(ns.metadata.name)
118-
logger.debug(f'Skipping namespaces: {ns.metadata.name} avoidpatrn: {avoidns}')
119-
# purge
120-
for ns in matchedns.copy():
121-
if ns in avoidedns:
122-
matchedns.remove(ns)
123-
124-
return matchedns
125-
126-
127-
def create_secret(logger,namespace,body,v1=None):
128-
"""Creates a given secret on a given namespace
129-
"""
130-
if v1 is None:
131-
v1 = client.CoreV1Api()
132-
logger.debug('new client - fn create secret')
133-
try:
134-
sec_name = body['metadata']['name']
135-
except KeyError:
136-
logger.debug("No name in body ?")
137-
raise kopf.TemporaryError("can not get the name.")
138-
try:
139-
data = body.get('data')
140-
except KeyError:
141-
data = ''
142-
logger.error("Empty secret?? could not get the data.")
143-
144-
if 'valueFrom' in data:
145-
if len(data.keys()) > 1:
146-
raise kopf.TemporaryError("ValueFrom can not coexist with other keys in the data")
147-
148-
try:
149-
ns_from = data['ValueFrom']['namespace']
150-
name_from = data['ValueFrom']['name']
151-
except KeyError:
152-
logger.error("Can not get Values from external secret")
153-
# to-do keys_from
154-
logger.debug(f'Take value from secret {name_from} from namespace {ns_from}')
155-
# data = read_data_secret(name,namespace)
156-
#here - doing the valuform thing. but first fix and update all.
157-
158-
secret_type = 'Opaque'
159-
if 'type' in body:
160-
secret_type = body['type']
161-
body = client.V1Secret()
162-
body.metadata = client.V1ObjectMeta(name=sec_name)
163-
body.type = secret_type
164-
body.data = data
165-
# kopf.adopt(body)
166-
logger.info(f"cloning secret in namespace {namespace}")
167-
try:
168-
api_response = v1.create_namespaced_secret(namespace, body)
169-
except client.rest.ApiException as e:
170-
if e.reason == 'Conflict':
171-
logger.warning(f"secret `{sec_name}` already exist in namesace '{namespace}'")
172-
return 0
173-
logger.error(f'Can not create a secret, it is base64 encoded? data: {data}')
174-
logger.error(f'Kube exception {e}')
175-
return 1
176-
return 0
177-
17887
@kopf.on.create('', 'v1', 'namespaces')
17988
async def namespace_watcher(spec,patch,logger,meta,body,**kwargs):
18089
"""Watch for namespace events

yaml/02_deployment.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ spec:
2020
# - name: regcred
2121
containers:
2222
- name: clustersecret
23-
image: flag5/clustersecret:0.0.7
23+
image: flag5/clustersecret:0.0.8-beta
2424
# imagePullPolicy: Always
2525
# Uncomment next lines for debug:
2626
# command:

yaml/Object_example/obj2.yaml

+1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ metadata:
66
namespace: clustersecret
77
matchNamespace:
88
- 'cluster.*'
9+
# it will be replicated in clustersecret namespace.
910
data:
1011
username: YWRtaW4=
1112
password: MWYyZDFlMmU2N2Rm
+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# A demo custom resource for the Kopf example operators.
2+
apiVersion: clustersecret.io/v1
3+
kind: ClusterSecret
4+
metadata:
5+
name: value-from
6+
namespace: clustersecret
7+
labels:
8+
somelabel: somevalue
9+
annotations:
10+
someannotation: somevalue
11+
matchNamespace:
12+
- '.*'
13+
avoidNamespaces:
14+
- 'default'
15+
- 'kube-system'
16+
# as output in kubectl create secret docker-registry regkey --docker-server my.docker.registry --docker-username adminuser --docker-password adminpass -o yaml --dry-run
17+
data:
18+
valueFrom:
19+
secretKeyRef:
20+
#it requires obj2.yaml first.
21+
name: global-secret2
22+
namespace: clustersecret

0 commit comments

Comments
 (0)