├── .gitignore ├── src ├── main │ ├── bash │ │ └── deploy_openunison.sh │ ├── docker │ │ └── Dockerfile │ └── java │ │ └── com │ │ └── tremolosecurity │ │ └── kubernetes │ │ └── artifacts │ │ ├── obj │ │ ├── HttpCon.java │ │ ├── X509Data.java │ │ └── CertificateData.java │ │ ├── run │ │ ├── TestCert.java │ │ ├── TestRun.java │ │ ├── RunWatch.java │ │ ├── RunDeployment.java │ │ └── Controller.java │ │ └── util │ │ ├── DbUtils.java │ │ ├── NetUtil.java │ │ ├── K8sWatcher.java │ │ ├── CertUtils.java │ │ └── K8sUtils.java └── test │ ├── java │ └── com │ │ └── tremolosecurity │ │ └── kubernetes │ │ └── artifacts │ │ └── TestCertUtils.java │ └── js │ └── test.js ├── README.md ├── ou-test-openunison.yaml ├── .factorypath ├── pom.xml └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | .vscode 3 | .classpath 4 | .project 5 | org.* 6 | .DS_Store -------------------------------------------------------------------------------- /src/main/bash/deploy_openunison.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | kubectl create namespace openunison-deploy 4 | kubectl create configmap extracerts --from-file $1 -n openunison-deploy 5 | kubectl create secret generic input --from-file $2 -n openunison-deploy 6 | 7 | kubectl create -f $3 8 | 9 | kubectl get pods -n openunison-deploy --watch -------------------------------------------------------------------------------- /src/test/java/com/tremolosecurity/kubernetes/artifacts/TestCertUtils.java: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Tremolo Security, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package com.tremolosecurity.kubernetes.artifacts; 16 | 17 | import org.junit.BeforeClass; 18 | import org.junit.Test; 19 | 20 | public class TestCertUtils { 21 | 22 | 23 | @BeforeClass 24 | public static void setUpBeforeClass() throws Exception { 25 | 26 | } 27 | 28 | @Test 29 | public void testNothing() { 30 | 31 | } 32 | 33 | } -------------------------------------------------------------------------------- /src/test/js/test.js: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Tremolo Security, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | print("Loading CertUtils"); 16 | var CertUtils = Java.type("com.tremolosecurity.kubernetes.artifacts.util.CertUtils"); 17 | print("Creating certInfo"); 18 | certInfo = { 19 | "serverName":"openunison.openunison.svc.cluster.local", 20 | "ou":"dev", 21 | "o":"tremolo", 22 | "l":"alexandria", 23 | "st":"virginia", 24 | "c":"us", 25 | "caCert":false 26 | } 27 | 28 | print("generating certificate"); 29 | var x509data = CertUtils.createCertificate(certInfo); 30 | 31 | print("printing cert"); 32 | print(CertUtils.exportCert(x509data.getCertificate())); 33 | print(CertUtils.generateCSR(x509data)); 34 | 35 | print(k8s.callWS("/api/v1/namespaces").data); -------------------------------------------------------------------------------- /src/main/docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:20.04 2 | 3 | MAINTAINER Tremolo Security, Inc. - Docker 4 | 5 | ENV JDK_VERSION=1.8.0 \ 6 | ARTIFACT_DEPLOY_VERSION=1.0.0 7 | 8 | LABEL io.k8s.description="Platform for deploying kubernetes artifacts" \ 9 | io.k8s.display-name="Kubernetes Artifact Deployer" 10 | 11 | RUN apt-get update;apt-get -y install openjdk-8-jdk-headless curl apt-transport-https gnupg && \ 12 | curl -s https://packages.cloud.google.com/apt/doc/apt-key.gpg | apt-key add - && \ 13 | echo "deb http://apt.kubernetes.io/ kubernetes-xenial main" > /etc/apt/sources.list.d/kubernetes.list && \ 14 | apt-get update; apt-get install -y kubectl ; apt-get -y upgrade;apt-get clean;rm -rf /var/lib/apt/lists/*; \ 15 | groupadd -r artifactdeploy -g 433 && \ 16 | mkdir /usr/local/artifactdeploy && \ 17 | useradd -u 431 -r -g artifactdeploy -d /usr/local/artifactdeploy -s /sbin/nologin -c "Artifact Deployment Docker image user" artifactdeploy && \ 18 | curl https://nexus.tremolo.io/repository/releases/com/tremolosecurity/kubernetes/artifact-deployment/$ARTIFACT_DEPLOY_VERSION/artifact-deployment-$ARTIFACT_DEPLOY_VERSION.jar -o /usr/local/artifactdeploy/artifact-deploy.jar && \ 19 | chown -R artifactdeploy:artifactdeploy /usr/local/artifactdeploy 20 | 21 | USER 431 22 | 23 | CMD ["/usr/bin/java", "-jar", "/usr/local/artifactdeploy/artifact-deploy.jar"] 24 | -------------------------------------------------------------------------------- /src/main/java/com/tremolosecurity/kubernetes/artifacts/obj/HttpCon.java: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Tremolo Security, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | 16 | package com.tremolosecurity.kubernetes.artifacts.obj; 17 | 18 | 19 | import org.apache.http.impl.client.CloseableHttpClient; 20 | import org.apache.http.impl.conn.BasicHttpClientConnectionManager; 21 | 22 | /** 23 | * Utility class representing an http connection 24 | */ 25 | public class HttpCon { 26 | 27 | 28 | 29 | BasicHttpClientConnectionManager bcm; 30 | CloseableHttpClient http; 31 | 32 | /** 33 | * Get client manager 34 | * @return 35 | */ 36 | public BasicHttpClientConnectionManager getBcm() { 37 | return bcm; 38 | } 39 | 40 | /** 41 | * Set client manager 42 | * @param bcm 43 | */ 44 | public void setBcm(BasicHttpClientConnectionManager bcm) { 45 | this.bcm = bcm; 46 | } 47 | 48 | /** 49 | * Get HTTP client 50 | * @return 51 | */ 52 | public CloseableHttpClient getHttp() { 53 | return http; 54 | } 55 | 56 | /** 57 | * Set http client 58 | */ 59 | public void setHttp(CloseableHttpClient http) { 60 | this.http = http; 61 | } 62 | 63 | 64 | } 65 | 66 | 67 | -------------------------------------------------------------------------------- /src/main/java/com/tremolosecurity/kubernetes/artifacts/run/TestCert.java: -------------------------------------------------------------------------------- 1 | package com.tremolosecurity.kubernetes.artifacts.run; 2 | 3 | import java.io.FileOutputStream; 4 | import java.util.LinkedHashMap; 5 | 6 | import com.tremolosecurity.kubernetes.artifacts.obj.CertificateData; 7 | import com.tremolosecurity.kubernetes.artifacts.obj.X509Data; 8 | import com.tremolosecurity.kubernetes.artifacts.util.CertUtils; 9 | 10 | import org.bouncycastle.jcajce.provider.asymmetric.rsa.PSSSignatureSpi.SHA256withRSA; 11 | import org.joda.time.DateTime; 12 | 13 | public class TestCert { 14 | public static void main(String[] args) throws Exception { 15 | System.out.println("here"); 16 | CertificateData cd = new CertificateData(); 17 | cd.setCaCert(true); 18 | cd.setRsa(true); 19 | cd.setNotBefore(new DateTime().toDate()); 20 | cd.setNotAfter(new DateTime().plusDays(365).toDate()); 21 | cd.setServerName("k8sou.apps.192-168-2-144.nip.io"); 22 | cd.setOu("testx"); 23 | cd.setO("test"); 24 | cd.setSigAlg("SHA256withRSA"); 25 | cd.setSize(2048); 26 | cd.setL("test"); 27 | cd.setC("test"); 28 | cd.setSt("test"); 29 | cd.setSubjectAlternativeNames(new LinkedHashMap()); 30 | cd.getSubjectAlternativeNames().add("k8sdb.apps.192-168-2-144.nip.io"); 31 | X509Data res = CertUtils.createCertificate(cd); 32 | 33 | FileOutputStream out = new FileOutputStream("/tmp/certs/tls.key"); 34 | out.write(CertUtils.exportKey(res.getKeyData().getPrivate()).getBytes("UTF-8")); 35 | out.close(); 36 | 37 | out = new FileOutputStream("/tmp/certs/tls.crt"); 38 | out.write(CertUtils.exportCert(res.getCertificate()).getBytes("UTF-8")); 39 | out.close(); 40 | 41 | 42 | 43 | } 44 | } -------------------------------------------------------------------------------- /src/main/java/com/tremolosecurity/kubernetes/artifacts/run/TestRun.java: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Tremolo Security, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package com.tremolosecurity.kubernetes.artifacts.run; 16 | 17 | import java.io.BufferedReader; 18 | import java.io.FileInputStream; 19 | import java.io.FileReader; 20 | import java.io.InputStreamReader; 21 | import java.security.Security; 22 | import java.util.HashMap; 23 | import java.util.Map; 24 | 25 | import javax.script.ScriptContext; 26 | import javax.script.ScriptEngine; 27 | import javax.script.ScriptEngineManager; 28 | 29 | import com.tremolosecurity.kubernetes.artifacts.util.K8sUtils; 30 | 31 | import org.bouncycastle.jce.provider.BouncyCastleProvider; 32 | 33 | /** 34 | * TestRun 35 | */ 36 | public class TestRun { 37 | 38 | public static void main(String[] args) throws Exception { 39 | /*K8sUtils k8s = new K8sUtils("/home/mlb/tmp/k8s/token","/home/mlb/tmp/k8s/master.pem","/home/mlb/tmp/k8s/extracerts","https://k8s-installer-master.tremolo.lan:6443"); 40 | 41 | Map inputParams = new HashMap(); 42 | 43 | BufferedReader in = new BufferedReader(new InputStreamReader(new FileInputStream("/home/mlb/tmp/k8s/props"))); 44 | String line; 45 | while ((line = in.readLine()) != null) { 46 | String name = line.substring(0,line.indexOf('=')); 47 | String val = line.substring(line.indexOf('=') + 1); 48 | inputParams.put(name, val); 49 | }*/ 50 | 51 | 52 | 53 | Security.addProvider(new BouncyCastleProvider()); 54 | ScriptEngine engine = new ScriptEngineManager().getEngineByName("nashorn"); 55 | //engine.getBindings(ScriptContext.ENGINE_SCOPE).put("k8s", k8s); 56 | //engine.getBindings(ScriptContext.ENGINE_SCOPE).put("inProp", inputParams); 57 | engine.eval(new FileReader("/Users/mlb/Documents/testxml.js")); 58 | 59 | 60 | } 61 | } -------------------------------------------------------------------------------- /src/main/java/com/tremolosecurity/kubernetes/artifacts/obj/X509Data.java: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Tremolo Security, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package com.tremolosecurity.kubernetes.artifacts.obj; 16 | 17 | import java.security.KeyPair; 18 | import java.security.cert.X509Certificate; 19 | 20 | /** 21 | * X509Data 22 | * Storage for both the certificate data and the keypair 23 | */ 24 | public class X509Data { 25 | X509Certificate certificate; 26 | KeyPair keyData; 27 | private CertificateData certInput; 28 | 29 | /** 30 | * Default constructor 31 | */ 32 | public X509Data() { 33 | 34 | } 35 | 36 | /** 37 | * Create with key material 38 | * @param kp 39 | * @param cert 40 | * @param certInput 41 | */ 42 | public X509Data(KeyPair kp,X509Certificate cert,CertificateData certInput) { 43 | this.keyData = kp; 44 | this.certificate = cert; 45 | this.certInput = certInput; 46 | } 47 | 48 | /** 49 | * @return the certificate 50 | */ 51 | public X509Certificate getCertificate() { 52 | return certificate; 53 | } 54 | 55 | /** 56 | * @return the keyData 57 | */ 58 | public KeyPair getKeyData() { 59 | return keyData; 60 | } 61 | 62 | /** 63 | * @param certificate the certificate to set 64 | */ 65 | public void setCertificate(X509Certificate certificate) { 66 | this.certificate = certificate; 67 | } 68 | 69 | /** 70 | * @param keyData the keyData to set 71 | */ 72 | public void setKeyData(KeyPair keyData) { 73 | this.keyData = keyData; 74 | } 75 | 76 | 77 | /** 78 | * @return the certInput 79 | */ 80 | public CertificateData getCertInput() { 81 | return certInput; 82 | } 83 | 84 | /** 85 | * @param certInput the certInput to set 86 | */ 87 | public void setCertInput(CertificateData certInput) { 88 | this.certInput = certInput; 89 | } 90 | } -------------------------------------------------------------------------------- /src/main/java/com/tremolosecurity/kubernetes/artifacts/run/RunWatch.java: -------------------------------------------------------------------------------- 1 | package com.tremolosecurity.kubernetes.artifacts.run; 2 | 3 | import com.tremolosecurity.kubernetes.artifacts.util.K8sUtils; 4 | import com.tremolosecurity.kubernetes.artifacts.util.K8sWatcher; 5 | 6 | /** 7 | * RunWatch 8 | */ 9 | public class RunWatch implements Runnable { 10 | K8sUtils k8s; 11 | boolean running; 12 | String uri; 13 | String functionName; 14 | 15 | boolean closeFromError; 16 | 17 | K8sWatcher watcher; 18 | 19 | public RunWatch(K8sUtils k8s, String uri, String functionName) { 20 | this.running = true; 21 | this.k8s = k8s; 22 | this.uri = uri; 23 | this.functionName = functionName; 24 | this.watcher = new K8sWatcher(k8s, this.functionName); 25 | } 26 | 27 | public boolean isRightVersion() { 28 | return this.watcher.isValidUri(this.uri); 29 | } 30 | 31 | @Override 32 | public void run() { 33 | closeFromError = false; 34 | while (running) { 35 | 36 | try { 37 | 38 | watcher.watchUri(uri); 39 | } catch (Exception e) { 40 | System.out.println("Error watching"); 41 | e.printStackTrace(); 42 | try { 43 | Thread.sleep(2000); 44 | } catch (InterruptedException e1) { 45 | 46 | } 47 | } 48 | 49 | /*try { 50 | k8s.watchURI(uri, functionName); 51 | } catch (Throwable t) { 52 | System.err.println("Error watching " + uri + "re-querying"); 53 | t.printStackTrace(System.err); 54 | 55 | String newUri = uri.substring(0, uri.indexOf('?')); 56 | try { 57 | this.uri = Controller.findResourceVersion(k8s, newUri); 58 | } catch (Exception e) { 59 | System.err.println("Could not get latest resource version uri"); 60 | e.printStackTrace(); 61 | this.running = false; 62 | this.closeFromError = true; 63 | } 64 | 65 | } 66 | 67 | try { 68 | Thread.sleep(1000); 69 | } catch (InterruptedException e) { 70 | 71 | }*/ 72 | 73 | } 74 | 75 | if (closeFromError) { 76 | System.out.println("Closing from a previous error"); 77 | System.exit(1); 78 | } 79 | 80 | } 81 | 82 | public void stopThread() { 83 | this.running = false; 84 | } 85 | 86 | } -------------------------------------------------------------------------------- /src/main/java/com/tremolosecurity/kubernetes/artifacts/util/DbUtils.java: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Tremolo Security, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package com.tremolosecurity.kubernetes.artifacts.util; 16 | 17 | import java.io.BufferedReader; 18 | import java.io.ByteArrayInputStream; 19 | import java.io.FileInputStream; 20 | import java.io.FileNotFoundException; 21 | import java.io.IOException; 22 | import java.io.InputStreamReader; 23 | import java.sql.Connection; 24 | import java.sql.DriverManager; 25 | import java.sql.SQLException; 26 | import java.sql.Statement; 27 | import java.util.ArrayList; 28 | import java.util.List; 29 | 30 | /** 31 | * DbUtils 32 | * Utilities for interacting with SQL databases 33 | */ 34 | public class DbUtils { 35 | 36 | 37 | /** 38 | * Run a list of sql statements againt a database 39 | * @param sqls 40 | * @param driver 41 | * @param url 42 | * @param userName 43 | * @param password 44 | * @throws ClassNotFoundException 45 | * @throws SQLException 46 | */ 47 | public static void runSQL(List sqls, String driver, String url, String userName, String password) 48 | throws ClassNotFoundException, SQLException { 49 | Class.forName(driver); 50 | Connection con = null; 51 | 52 | if (url.toLowerCase().contains("authentication=activedirectoryintegrated") || url.toLowerCase().contains("authentication=activedirectorymsi") || url.toLowerCase().contains("integratedsecurity=true")) { 53 | // we're using AD, no username or password 54 | con = DriverManager.getConnection(url); 55 | } else { 56 | con = DriverManager.getConnection(url, userName, password); 57 | } 58 | 59 | 60 | Statement stmt = con.createStatement(); 61 | for (String sql : sqls) { 62 | System.out.println("sql : '" + sql + "'"); 63 | stmt.execute(sql); 64 | } 65 | 66 | stmt.close(); 67 | con.close(); 68 | } 69 | 70 | /** 71 | * Parse a list of SQL statements from the source of a file 72 | * @param source 73 | * @return 74 | * @throws IOException 75 | */ 76 | public static List parseSQL(String source) throws IOException { 77 | ArrayList sqlStatements = new ArrayList(); 78 | 79 | BufferedReader in = new BufferedReader(new InputStreamReader(new ByteArrayInputStream(source.getBytes("UTF-8")))); 80 | 81 | String line; 82 | String sql = null; 83 | 84 | while ((line = in.readLine()) != null) { 85 | if (sql == null) { 86 | if (line.trim().length() == 0 || line.trim().startsWith("#") || line.trim().startsWith("-")) { 87 | continue; 88 | } else { 89 | sql = line; 90 | } 91 | } else { 92 | sql += " " + line; 93 | 94 | } 95 | 96 | if (sql.trim().endsWith(";")) { 97 | sql = sql.trim(); 98 | sql = sql.substring(0,sql.lastIndexOf(';')); 99 | sqlStatements.add(sql); 100 | sql = null; 101 | } 102 | } 103 | 104 | 105 | 106 | return sqlStatements; 107 | 108 | } 109 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # kubernetes-javascript-operator 2 | Framework for Building Operators in Javascript 3 | 4 | ## Objectives 5 | 6 | * Provide the building blocks for building an operator out of JavaScript and Java. 7 | * The base container can be reused across operator implementations 8 | * JavaScript is attached as `ConfigMap`s for the image 9 | * Image is built on a Java base, providing access to both JavaScript functions and Java's built in capabilities 10 | * Provide low level access to the api server with some error handling 11 | * Watch single object type on startup, let additional watches be registered in code 12 | * Compatibility With Red Hat Operator Lifecycle Manager 13 | 14 | ## Why JavaScript? 15 | 16 | * Well known across IT environments 17 | * Does not require pre-compiled binaries 18 | * Native support for JSON 19 | * Runs in Java 20 | 21 | ## Why Java? 22 | 23 | * Robust APIs 24 | * Well tested and trusted in IT 25 | * Flexible 26 | 27 | ## Creating an Operator 28 | 29 | The JavaScript operator is an application written in Java that loads JavaScript files. In order to create an operator you must have at least one javascript file with a function called `on_watch`. Here is an example from the OpenUnison operator (https://github.com/TremoloSecurity/openunison-k8s-operator/blob/master/src/main/js/operator.js): 30 | 31 | ``` 32 | //Called by controller 33 | function on_watch(k8s_event) { 34 | print("in js : " + k8s_event); 35 | event_json = JSON.parse(k8s_event); 36 | k8s_obj = event_json['object']; 37 | cfg_obj = k8s_obj['spec']; 38 | 39 | if (event_json["type"] === "ADDED") { 40 | generate_openunison_secret(event_json); 41 | create_static_objects(); 42 | 43 | 44 | 45 | } else if (event_json["type"] === "MODIFIED") { 46 | generate_openunison_secret(event_json); 47 | update_k8s_deployment(); 48 | 49 | } else if (event_json["type"] === "DELETED") { 50 | delete_k8s_deployment(); 51 | } 52 | } 53 | ``` 54 | 55 | You can have multiple javascript files to organize your code, they just all need to be in the same directory. See OpenUnison's operator for an example of how to build your operator - https://github.com/TremoloSecurity/openunison-k8s-operator/tree/master/src/main/js 56 | 57 | ## Testing Your Operator 58 | 59 | Once of the benefits of using JavaScript and Java as the base for your operator is the ability to test from outside the cluster easily. This cuts down your development cycles by minimizing time creating containers, deploying and updating in your cluster. 60 | 61 | 1. Create a directory to store your Kubernetes information (certificates, tokens, etc). 62 | 2. Create a directory in this directory called `extra` 63 | 3. Create a service account, grant it the appropriate privileges then get its token by running `kubectl describe secret sa-name-token-xxxxx`. Once you have the token, store it in a file called `token` in the directory from step 1 64 | 4. Get your cluster's CA certificate, store it in a file called `ca.pem` in the directory from step 1 65 | 5. Download the operator jar from https://nexus.tremolo.io/repository/betas/com/tremolosecurity/kubernetes/javascript-operator/1.0.0/javascript-operator-1.0.0.jar 66 | 67 | Once you are ready to test, run the operator locally: 68 | 69 | `java -jar /path/to/javascript-operator-1.0.0.jar -tokenPath /path/to/k8s-operator/token -rootCaPath /path/to/k8s-operator/ca.pem -configMaps /path/to/k8s-operator/extra -kubernetesURL https://192.168.2.132:6443 -namespace openunison -apiGroup 'openunison.tremolo.io/v1' -objectType openunisons -jsPath '/path/to/openunison-k8s-operator/src/main/js'` 70 | 71 | As you create/update/delete your custom resources that are being watched you'll see the output. 72 | 73 | ## Deploying Your Operator 74 | 75 | First, create a Docker image. The Dockerfile for the OpenUnison operator is a good start - https://github.com/TremoloSecurity/openunison-k8s-operator/blob/master/Dockerfile. Once you have pushed your Dockerfile into a registry, create a `Deployment` that will run in your namespace. The example from OpenUnison is a good starting point - https://github.com/TremoloSecurity/openunison-k8s-operator/blob/master/src/main/yaml/openunison-operator-deployment.yaml. 76 | 77 | ## Toolbox 78 | 79 | From inside of your JavaScript there are multiple objects meant to make it easier to interact with the api server. See the `docs` folder for a detailed description of the objects definitions. 80 | 81 | ### k8s 82 | 83 | The `k8s` object maintains a "connection" to the api server. Internally it trusts the api server's certificate and is pre-configured to work with the api token for the service account of the container. 84 | -------------------------------------------------------------------------------- /ou-test-openunison.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: openunison.tremolo.io/v1 2 | kind: OpenUnison 3 | metadata: 4 | creationTimestamp: "2019-03-06T16:05:49Z" 5 | generation: 1 6 | name: test-openunison 7 | namespace: openunison-operator 8 | resourceVersion: "12081148" 9 | selfLink: /apis/openunison.tremolo.io/v1/namespaces/openunison-operator/openunisons/test-openunison 10 | uid: b5b6c45b-4029-11e9-8fac-525400858745 11 | spec: 12 | activemq_image: docker.io/tremolosecurity/activemq-docker:latest 13 | dest_secret: openunison 14 | enable_activemq: false 15 | image: docker.io/tremolosecurity/openunison-k8s-login-oidc:latest 16 | key_store: 17 | key_pairs: 18 | create_keypair_template: 19 | - name: ou 20 | value: k8s 21 | - name: o 22 | value: Tremolo Security 23 | - name: l 24 | value: Alexandria 25 | - name: st 26 | value: Virginia 27 | - name: c 28 | value: US 29 | keys: 30 | - create_data: 31 | ca_cert: false 32 | key_size: 2048 33 | server_name: ${k8s_obj.metadata.name + '.' + k8s_namespace + '.svc.cluster.local'} 34 | sign_by_k8s_ca: true 35 | subject_alternative_names: [] 36 | import_into_ks: keypair 37 | name: unison-tls 38 | tls_secret_name: unison-tls-secret 39 | - create_data: 40 | ca_cert: false 41 | key_size: 2048 42 | server_name: unison-saml2-rp-sig 43 | sign_by_k8s_ca: false 44 | subject_alternative_names: [] 45 | import_into_ks: keypair 46 | name: unison-saml2-rp-sig 47 | tls_secret_name: unison-saml2-rp-sig 48 | - create_data: 49 | ca_cert: false 50 | key_size: 2048 51 | server_name: ${inProp['OU_HOST']} 52 | sign_by_k8s_ca: false 53 | subject_alternative_names: 54 | - ${inProp['K8S_DASHBOARD_HOST']} 55 | import_into_ks: none 56 | name: ou-tls-certificate 57 | - create_data: 58 | ca_cert: false 59 | key_size: 2048 60 | secret_info: 61 | cert_nanme: db.crt 62 | key_name: db.key 63 | type_of_secret: Opaque 64 | server_name: kubernetes-dashboard.kube-system.svc.cluster.local 65 | sign_by_k8s_ca: true 66 | subject_alternative_names: [] 67 | target_namespace: kube-system 68 | import_into_ks: none 69 | name: kubernetes-dashboard.kube-system.svc.cluster.local 70 | replace_if_exists: true 71 | static_keys: 72 | - name: session-unison 73 | version: 2 74 | - name: lastmile-oidc 75 | version: 1 76 | trusted_certificates: 77 | - name: trusted-adldaps 78 | pem_data: |- 79 | -----BEGIN CERTIFICATE----- 80 | MIIDNDCCAhygAwIBAgIQbRNj6RKqtqVPvW65qZxXXjANBgkqhkiG9w0BAQUFADAi 81 | MSAwHgYDVQQDDBdBREZTLkVOVDJLMTIuRE9NQUlOLkNPTTAeFw0xNDAzMjgwMTA1 82 | MzNaFw0yNDAzMjUwMTA1MzNaMCIxIDAeBgNVBAMMF0FERlMuRU5UMksxMi5ET01B 83 | SU4uQ09NMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2s9JkeNAHOkQ 84 | 1QYJgjefUwcaogEMcaW/koA+bu9xbr4rHy/2gN/kc8OkoPuwJ/nNlOIO+s+MbnXS 85 | L9mUTC4OK7trkEjiKXB+D+VSYy6imXh6zpBtNbeZyx+rdBnaOv3ByZRnnEB8LmhM 86 | vHA+4f/t9fx/2vt6wPx//VgIq9yuYYUQRLm1WjyUBFrZeGoSpPm0Kewm+B0bhmMb 87 | dyC+3fhaKC+Uk1NPodE2973jLBZJelZxsZY40Ww8zYQwdGYIbXqoTc+1a/x4f1En 88 | m4ANqggHtw+Nq8zhss3yTtY+UYKDRBILdLVZQhHJExe0kAeisgMxI/bBwO1HbrFV 89 | +zSnk+nvgQIDAQABo2YwZDAzBgNVHSUELDAqBggrBgEFBQcDAQYIKwYBBQUHAwIG 90 | CisGAQQBgjcUAgIGCCsGAQUFBwMDMB0GA1UdDgQWBBTyJUfY66zYbm9i0xeYHuFI 91 | 4MN7uDAOBgNVHQ8BAf8EBAMCBSAwDQYJKoZIhvcNAQEFBQADggEBAM5kz9OKNSuX 92 | 8w4NOgnfIFdazd0nPlIUbvDVfQoNy9Q0S1SFUVMekIPNiVhfGzya9IwRtGb1VaBQ 93 | AQ2ORIzHr8A2r5UNLx3mFjpJmeOxQwlV0X+g8s+253KVFxOpRE6yyagn/BxxptTL 94 | a1Z4qeQJLD42ld1qGlRwFtVRmVFZzVXVrpu7NuFd3vlnnO/qKWXU+uMsfXtsl13n 95 | ec1kw1Ewq2jnK8WImKTQ7/9WbaIY0gx8mowCJSOsRq0TE7zK/N55drN1wXJVxWe5 96 | 4N32eCqotXy9j9lzdkNa7awb9q38nWVxP+va5jqNIDlljB6tExy5n3s7t6KK6g5j 97 | TZgVqrZ3+ms= 98 | -----END CERTIFICATE----- 99 | non_secret_data: 100 | - name: AD_BASE_DN 101 | value: cn=users,dc=ent2k12,dc=domain,dc=com 102 | - name: AD_BIND_DN 103 | value: cn=Administrator,cn=users,dc=ent2k12,dc=domain,dc=com 104 | - name: AD_CON_TYPE 105 | value: ldaps 106 | - name: AD_HOST 107 | value: 192.168.2.75 108 | - name: AD_PORT 109 | value: "636" 110 | - name: K8S_DASHBOARD_HOST 111 | value: k8sdb.tremolo.lan 112 | - name: K8S_URL 113 | value: https://k8s-installer-master.tremolo.lan:6443 114 | - name: OU_COOKIE_DOMAIN 115 | value: tremolo.lan 116 | - name: OU_HOST 117 | value: k8sou.tremolo.lan 118 | - name: SRV_DNS 119 | value: "false" 120 | - name: SESSION_INACTIVITY_TIMEOUT_SECONDS 121 | value: "900" 122 | - name: MYVD_CONFIG_PATH 123 | value: WEB-INF/myvd.conf 124 | openunison_network_configuration: 125 | activemq_dir: /tmp/amq 126 | allowed_client_names: [] 127 | ciphers: 128 | - TLS_RSA_WITH_RC4_128_SHA 129 | - TLS_RSA_WITH_AES_128_CBC_SHA 130 | - TLS_RSA_WITH_AES_256_CBC_SHA 131 | - TLS_RSA_WITH_3DES_EDE_CBC_SHA 132 | - TLS_RSA_WITH_AES_128_CBC_SHA256 133 | - TLS_RSA_WITH_AES_256_CBC_SHA256 134 | client_auth: none 135 | force_to_secure: true 136 | open_external_port: 80 137 | open_port: 8080 138 | path_to_deployment: /usr/local/openunison/work 139 | path_to_env_file: /etc/openunison/ou.env 140 | quartz_dir: /tmp/quartz 141 | secure_external_port: 443 142 | secure_key_alias: unison-tls 143 | secure_port: 8443 144 | replicas: 2 145 | secret_data: 146 | - unisonKeyStorePassword 147 | - AD_BIND_PASSWORD 148 | source_secret: openunison-secrets-source 149 | -------------------------------------------------------------------------------- /src/main/java/com/tremolosecurity/kubernetes/artifacts/run/RunDeployment.java: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Tremolo Security, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package com.tremolosecurity.kubernetes.artifacts.run; 16 | 17 | import java.io.BufferedReader; 18 | import java.io.FileInputStream; 19 | import java.io.InputStreamReader; 20 | import java.net.URL; 21 | import java.net.URLConnection; 22 | import java.nio.charset.StandardCharsets; 23 | import java.security.Security; 24 | import java.util.HashMap; 25 | import java.util.Map; 26 | import java.util.stream.Collectors; 27 | 28 | import javax.script.ScriptContext; 29 | import javax.script.ScriptEngine; 30 | import javax.script.ScriptEngineManager; 31 | 32 | import com.tremolosecurity.kubernetes.artifacts.util.K8sUtils; 33 | import com.tremolosecurity.kubernetes.artifacts.util.NetUtil; 34 | 35 | import org.apache.commons.cli.CommandLine; 36 | import org.apache.commons.cli.CommandLineParser; 37 | import org.apache.commons.cli.DefaultParser; 38 | import org.apache.commons.cli.HelpFormatter; 39 | import org.apache.commons.cli.Options; 40 | import org.bouncycastle.jce.provider.BouncyCastleProvider; 41 | 42 | /** 43 | * RunDeployment 44 | */ 45 | public class RunDeployment { 46 | 47 | public static void main(String[] args) throws Exception { 48 | Options options = new Options(); 49 | options.addOption("tokenPath", true, "The path to the token to use when communicating with the API server"); 50 | options.addOption("rootCaPath", true, "The path to the certificate athority PEM file for the kubrnetes API server"); 51 | options.addOption("extraCertsPath", true, "The full path to a directory containing additional certificates to trust"); 52 | options.addOption("kubernetesURL", true, "The URL for the kubernetes api server"); 53 | options.addOption("installScriptURL", true, "The url of the install javascript"); 54 | options.addOption("secretsPath", true, "The path to the file containing all inputs"); 55 | options.addOption("help", false, "Prints this message"); 56 | options.addOption("deploymentTemplate",true,"URL for the kubernetes deployment template to generate final deployment yaml"); 57 | 58 | CommandLineParser parser = new DefaultParser(); 59 | CommandLine cmd = parser.parse(options, args,true); 60 | 61 | if (args.length == 0 || cmd.hasOption("help")) { 62 | HelpFormatter formatter = new HelpFormatter(); 63 | formatter.printHelp( "OpenUnison Kubernetes Artifact Deployer", options ); 64 | } else { 65 | String tokenPath = loadOption(cmd, "tokenPath", options); 66 | String rootCaPath = loadOption(cmd,"rootCaPath",options); 67 | String extraCertsPath = loadOption(cmd, "extraCertsPath", options); 68 | String kubernetesURL = loadOption(cmd, "kubernetesURL", options); 69 | String installScriptURL = loadOption(cmd,"installScriptURL",options); 70 | String secretsPath = loadOption(cmd, "secretsPath", options); 71 | String deploymentTemplate = cmd.getOptionValue("deploymentTemplate"); 72 | 73 | K8sUtils k8s = new K8sUtils(tokenPath,rootCaPath,extraCertsPath,kubernetesURL); 74 | 75 | NetUtil.initialize(extraCertsPath); 76 | 77 | Map inputParams = new HashMap(); 78 | 79 | BufferedReader in = new BufferedReader(new InputStreamReader(new FileInputStream(secretsPath))); 80 | String line; 81 | while ((line = in.readLine()) != null) { 82 | String name = line.substring(0,line.indexOf('=')); 83 | String val = line.substring(line.indexOf('=') + 1); 84 | inputParams.put(name, val); 85 | } 86 | 87 | String templateForDeployment = null; 88 | 89 | if (deploymentTemplate != null) { 90 | URL urlObj = new URL(deploymentTemplate); 91 | URLConnection conn = urlObj.openConnection(); 92 | try (BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8))) 93 | { 94 | templateForDeployment = reader.lines().collect(Collectors.joining("\n")); 95 | } 96 | } 97 | 98 | Security.addProvider(new BouncyCastleProvider()); 99 | ScriptEngine engine = new ScriptEngineManager().getEngineByName("nashorn"); 100 | engine.getBindings(ScriptContext.ENGINE_SCOPE).put("deploymentTemplate", templateForDeployment); 101 | engine.getBindings(ScriptContext.ENGINE_SCOPE).put("k8s", k8s); 102 | engine.getBindings(ScriptContext.ENGINE_SCOPE).put("inProp", inputParams); 103 | 104 | 105 | k8s.setEngine(engine); 106 | 107 | 108 | URL scriptURL = new URL(installScriptURL); 109 | engine.eval(new BufferedReader(new InputStreamReader(scriptURL.openStream()))); 110 | } 111 | } 112 | 113 | static String loadOption(CommandLine cmd,String name,Options options) { 114 | String val = cmd.getOptionValue(name); 115 | if (val == null) { 116 | System.err.println("Could not find option '" + name + "'"); 117 | HelpFormatter formatter = new HelpFormatter(); 118 | formatter.printHelp( "OpenUnison Kubernetes Artifact Deployer", options ); 119 | System.exit(1); 120 | return null; 121 | } else { 122 | return val; 123 | } 124 | } 125 | } -------------------------------------------------------------------------------- /src/main/java/com/tremolosecurity/kubernetes/artifacts/obj/CertificateData.java: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Tremolo Security, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package com.tremolosecurity.kubernetes.artifacts.obj; 16 | 17 | import java.text.SimpleDateFormat; 18 | import java.util.ArrayList; 19 | import java.util.Date; 20 | import java.util.LinkedHashMap; 21 | import java.util.List; 22 | 23 | /** 24 | * CertificateData 25 | */ 26 | public class CertificateData { 27 | 28 | String serverName = ""; 29 | String ou = ""; 30 | String o = ""; 31 | String l = ""; 32 | String st = ""; 33 | String c = ""; 34 | 35 | int size = 2048; 36 | boolean rsa = true; 37 | 38 | String sigAlg = "SHA256withRSA"; 39 | Date notBefore = new Date(System.currentTimeMillis()); 40 | Date notAfter = new Date(System.currentTimeMillis() + 31536000000L); 41 | 42 | SimpleDateFormat sdf = new SimpleDateFormat("MM/dd/yyyy"); 43 | 44 | boolean caCert; 45 | 46 | List subjectAlternativeNames; 47 | 48 | /** 49 | * Properly escaoe an an RDN in an X509 subject 50 | * @param rdn 51 | * @return 52 | */ 53 | public static String escpaeRDN(String rdn) { 54 | return rdn.replaceAll("[,]", "\\\\,").replaceAll("[+]", "\\\\+").replaceAll("[=]", "\\\\="); 55 | } 56 | 57 | /** 58 | * Default constructor 59 | */ 60 | public CertificateData() { 61 | this.subjectAlternativeNames = new ArrayList(); 62 | } 63 | 64 | /** 65 | * True if this certificate is for signing other certificates 66 | * @return 67 | */ 68 | public boolean isCaCert() { 69 | return this.caCert; 70 | } 71 | 72 | /** 73 | * Set to true if this certificate is meant for signing other certificates 74 | * @param caCert 75 | */ 76 | public void setCaCert(boolean caCert) { 77 | this.caCert = caCert; 78 | } 79 | 80 | /** 81 | * The server name for this certificate. Will be used for both the CN of the subject and as a subject alternative namne 82 | * @return 83 | */ 84 | public String getServerName() { 85 | return serverName; 86 | } 87 | 88 | /** 89 | * The server name for this certificate. Will be used for both the CN of the subject and as a subject alternative namne 90 | * @param serverName 91 | */ 92 | public void setServerName(String serverName) { 93 | this.serverName = CertificateData.escpaeRDN(serverName); 94 | } 95 | 96 | /** 97 | * For X509 Subject 98 | */ 99 | public String getOu() { 100 | return ou; 101 | } 102 | 103 | public void setOu(String ou) { 104 | this.ou = CertificateData.escpaeRDN(ou); 105 | } 106 | 107 | /** 108 | * For X509 Subject 109 | */ 110 | public String getO() { 111 | return o; 112 | } 113 | 114 | /** 115 | * For X509 Subject 116 | */ 117 | public void setO(String o) { 118 | this.o = CertificateData.escpaeRDN(o); 119 | } 120 | 121 | /** 122 | * Sets if this is an RSA or DSA certificate 123 | * @return 124 | */ 125 | public boolean isRsa() { 126 | return rsa; 127 | } 128 | 129 | /** 130 | * Gets if this is an RSA or DSA certificate 131 | * @return 132 | */ 133 | public void setRsa(boolean rsa) { 134 | this.rsa = rsa; 135 | } 136 | 137 | /** 138 | * The name of the signing algorithm 139 | * @return 140 | */ 141 | public String getSigAlg() { 142 | return sigAlg; 143 | } 144 | 145 | /** 146 | * The name of the signing algorithim 147 | * @param sigAlg 148 | */ 149 | public void setSigAlg(String sigAlg) { 150 | this.sigAlg = sigAlg; 151 | } 152 | 153 | /** 154 | * The date this certificate starts being valid 155 | * @return 156 | */ 157 | public Date getNotBefore() { 158 | return notBefore; 159 | } 160 | 161 | /** 162 | * The date this certificate starts being valid 163 | * @return 164 | */ 165 | public void setNotBefore(Date notBefore) { 166 | this.notBefore = notBefore; 167 | } 168 | 169 | /** 170 | * String version of not-before date 171 | * @return 172 | */ 173 | public String getNotBeforeStr() { 174 | return sdf.format(notBefore); 175 | } 176 | 177 | /** 178 | * String version of not before date 179 | * @param notBefore 180 | * @throws Exception 181 | */ 182 | public void setNotBeforeStr(String notBefore) throws Exception { 183 | this.notBefore = sdf.parse(notBefore); 184 | } 185 | 186 | /** 187 | * Date this certificate expires 188 | * @return 189 | */ 190 | public Date getNotAfter() { 191 | return notAfter; 192 | } 193 | 194 | /** 195 | * Date this certificate expires 196 | * @param notAfter 197 | */ 198 | public void setNotAfter(Date notAfter) { 199 | this.notAfter = notAfter; 200 | } 201 | 202 | /** 203 | * String version of not after 204 | * @return 205 | */ 206 | public String getNotAfterStr() { 207 | return sdf.format(notAfter); 208 | } 209 | 210 | /** 211 | * String version of not after 212 | * @param notAfter 213 | * @throws Exception 214 | */ 215 | public void setNotAfterStr(String notAfter) throws Exception { 216 | 217 | this.notAfter = sdf.parse(notAfter); 218 | 219 | } 220 | 221 | /** 222 | * For X509 Subject 223 | * @return 224 | */ 225 | public String getL() { 226 | return l; 227 | } 228 | 229 | /** 230 | * For X509 Subject 231 | * @param l 232 | */ 233 | public void setL(String l) { 234 | this.l = CertificateData.escpaeRDN(l); 235 | } 236 | 237 | /** 238 | * For X509 Subject 239 | * @return 240 | */ 241 | public String getSt() { 242 | return st; 243 | } 244 | 245 | /** 246 | * For X509 Subject 247 | * @param st 248 | */ 249 | public void setSt(String st) { 250 | this.st = CertificateData.escpaeRDN(st); 251 | } 252 | 253 | /** 254 | * For X509 Subject 255 | * @return 256 | */ 257 | public String getC() { 258 | return c; 259 | } 260 | 261 | /** 262 | * For X509 263 | * @param c 264 | */ 265 | public void setC(String c) { 266 | this.c = CertificateData.escpaeRDN(c); 267 | } 268 | 269 | /** 270 | * Key size 271 | * @return 272 | */ 273 | public int getSize() { 274 | return size; 275 | } 276 | 277 | /** 278 | * key size 279 | * @param size 280 | */ 281 | public void setSize(int size) { 282 | this.size = size; 283 | } 284 | 285 | /** 286 | * @return the subjectAlternativeNames 287 | */ 288 | public List getSubjectAlternativeNames() { 289 | return subjectAlternativeNames; 290 | } 291 | 292 | /** 293 | * @param subjectAlternativeNames the subjectAlternativeNames to set 294 | */ 295 | public void setSubjectAlternativeNames(LinkedHashMap vals) { 296 | 297 | this.subjectAlternativeNames = new ArrayList(); 298 | for (Object o : vals.values()) { 299 | this.subjectAlternativeNames.add((String) o); 300 | } 301 | 302 | 303 | } 304 | } -------------------------------------------------------------------------------- /.factorypath: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 8 | 10 | 4.0.0 11 | com.tremolosecurity.kubernetes 12 | javascript-operator 13 | 1.6.0 14 | javascript-operator 15 | jar 16 | 17 | 18 | 19 | 3.2.2 20 | 1.4 21 | 2.12.0 22 | 1.70 23 | 1.70 24 | 2.11.0 25 | 1.64 26 | 2.0.5 27 | 2.11.2 28 | 1.15 29 | 4.5.13 30 | 4.4.15 31 | 4.5.13 32 | 2.13.4 33 | 2.13.4 34 | 1.5.0 35 | 3.0.8 36 | 8.0.30 37 | 11.2.1.jre11 38 | 2.11.0 39 | 42.5.0 40 | 14.0.0 41 | 42 | 43 | 44 | 45 | tremolosecurity-dependencies 46 | https://nexus.tremolo.io/repository/dependencies/ 47 | 48 | 49 | nexus.tremolo.io 50 | tremolo.io-betas 51 | https://nexus.tremolo.io/repository/betas/ 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | nexus.tremolo.io 65 | tremolo.io-releases 66 | s3://tremolosecurity-maven/repository/betas 67 | 68 | 69 | 70 | 71 | 72 | junit 73 | junit 74 | 4.13.2 75 | test 76 | 77 | 78 | joda-time 79 | joda-time 80 | ${joda-time.version} 81 | 82 | 83 | org.bouncycastle 84 | bcprov-jdk15on 85 | ${bcprov-jdk15on.version} 86 | 87 | 88 | org.bouncycastle 89 | bcprov-ext-jdk15on 90 | ${bcprov-ext-jdk15on.version} 91 | 92 | 93 | org.bouncycastle 94 | bcpkix-jdk15on 95 | ${bcprov-ext-jdk15on.version} 96 | 97 | 98 | commons-codec 99 | commons-codec 100 | ${commons-codec.version} 101 | 102 | 103 | org.apache.httpcomponents 104 | httpclient 105 | ${httpclient.version} 106 | 107 | 108 | org.apache.httpcomponents 109 | httpcore 110 | ${httpcore.version} 111 | 112 | 113 | org.apache.httpcomponents 114 | httpmime 115 | ${httpmime.version} 116 | 117 | 118 | com.fasterxml.jackson.core 119 | jackson-databind 120 | ${jackson.version} 121 | 122 | 123 | com.fasterxml.jackson.dataformat 124 | jackson-dataformat-yaml 125 | ${jackson-core.version} 126 | 127 | 128 | commons-cli 129 | commons-cli 130 | ${commons-cli.version} 131 | 132 | 133 | org.mariadb.jdbc 134 | mariadb-java-client 135 | ${mariadb.version} 136 | 137 | 138 | 139 | mysql 140 | mysql-connector-java 141 | ${mysql.version} 142 | 143 | 144 | 145 | com.microsoft.sqlserver 146 | mssql-jdbc 147 | ${mssql.version} 148 | 149 | 150 | commons-io 151 | commons-io 152 | ${commonsio.version} 153 | 154 | 155 | com.googlecode.json-simple 156 | json-simple 157 | 1.1.1 158 | 159 | 160 | org.postgresql 161 | postgresql 162 | ${postgres.version} 163 | 164 | 165 | io.kubernetes 166 | client-java 167 | ${k8sapi.version} 168 | 169 | 170 | 171 | 172 | 173 | 174 | com.gkatzioura.maven.cloud 175 | s3-storage-wagon 176 | 2.3 177 | 178 | 179 | 180 | 181 | org.apache.maven.plugins 182 | maven-javadoc-plugin 183 | 3.0.1 184 | 185 | com.tremolosecurity.kubernetes.artifacts.run 186 | -Xdoclint:none 187 | none 188 | 189 | 190 | 191 | org.apache.maven.plugins 192 | maven-jar-plugin 193 | 2.2 194 | 195 | 196 | 197 | 198 | org.apache.maven.plugins 199 | maven-shade-plugin 200 | 2.3 201 | 202 | 203 | 205 | 206 | com.tremolosecurity.kubernetes.artifacts.run.Controller 207 | 208 | 209 | false 210 | 211 | 212 | 213 | *:* 214 | 215 | META-INF/*.SF 216 | META-INF/*.DSA 217 | META-INF/*.RSA 218 | 219 | 220 | 221 | 222 | 223 | 224 | package 225 | 226 | shade 227 | 228 | 229 | 230 | 231 | 232 | 233 | org.apache.maven.plugins 234 | maven-compiler-plugin 235 | 236 | 11 237 | 11 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | maven-project-info-reports-plugin 247 | 248 | false 249 | 250 | 251 | 252 | 253 | 254 | -------------------------------------------------------------------------------- /src/main/java/com/tremolosecurity/kubernetes/artifacts/run/Controller.java: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Tremolo Security, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package com.tremolosecurity.kubernetes.artifacts.run; 16 | 17 | import java.io.BufferedReader; 18 | import java.io.File; 19 | import java.io.FileInputStream; 20 | import java.io.IOException; 21 | import java.io.InputStreamReader; 22 | import java.net.MalformedURLException; 23 | import java.net.URL; 24 | import java.net.URLConnection; 25 | import java.nio.charset.StandardCharsets; 26 | import java.security.Security; 27 | import java.util.ArrayList; 28 | import java.util.HashMap; 29 | import java.util.List; 30 | import java.util.Map; 31 | import java.util.StringTokenizer; 32 | import java.util.stream.Collectors; 33 | 34 | import javax.script.ScriptContext; 35 | import javax.script.ScriptEngine; 36 | import javax.script.ScriptEngineManager; 37 | import javax.script.ScriptException; 38 | 39 | import com.tremolosecurity.kubernetes.artifacts.util.K8sUtils; 40 | import com.tremolosecurity.kubernetes.artifacts.util.NetUtil; 41 | 42 | import org.apache.commons.cli.CommandLine; 43 | import org.apache.commons.cli.CommandLineParser; 44 | import org.apache.commons.cli.DefaultParser; 45 | import org.apache.commons.cli.HelpFormatter; 46 | import org.apache.commons.cli.Options; 47 | import org.bouncycastle.jce.provider.BouncyCastleProvider; 48 | import org.json.simple.JSONObject; 49 | import org.json.simple.parser.JSONParser; 50 | import org.json.simple.parser.ParseException; 51 | 52 | /** 53 | * Controller 54 | */ 55 | public class Controller { 56 | 57 | static boolean stillWatching; 58 | 59 | public static String tokenPath; 60 | public static String rootCaPath; 61 | public static String configMaps; 62 | public static String kubernetesURL; 63 | public static String jsPath; 64 | public static String namespace; 65 | 66 | static List watches; 67 | 68 | public static void main(String[] args) throws Exception { 69 | watches = new ArrayList(); 70 | 71 | 72 | Runtime.getRuntime().addShutdownHook(new Thread() { 73 | public void run() { 74 | System.out.println("Cought the shutdown hook"); 75 | stillWatching = false; 76 | } 77 | }); 78 | 79 | Options options = new Options(); 80 | options.addOption("tokenPath", true, "The path to the token to use when communicating with the API server"); 81 | options.addOption("rootCaPath", true, 82 | "The path to the certificate athority PEM file for the kubrnetes API server"); 83 | options.addOption("configMaps", true, 84 | "The full path to a directory containing additional certificates to trust"); 85 | options.addOption("kubernetesURL", true, "The URL for the kubernetes api server"); 86 | options.addOption("jsPath", true, "Path to JavaScript files to load"); 87 | options.addOption("apiGroup", true, "version and group"); 88 | options.addOption("namespace", true, "namespace"); 89 | options.addOption("objectType", true, "CRD type"); 90 | 91 | options.addOption("help", false, "Prints this message"); 92 | 93 | CommandLineParser parser = new DefaultParser(); 94 | CommandLine cmd = parser.parse(options, args, true); 95 | 96 | stillWatching = true; 97 | 98 | if (args.length == 0 || cmd.hasOption("help")) { 99 | HelpFormatter formatter = new HelpFormatter(); 100 | formatter.printHelp("Kubernetes Javascript Operator Options", options); 101 | } else { 102 | tokenPath = loadOption(cmd, "tokenPath", options); 103 | rootCaPath = loadOption(cmd, "rootCaPath", options); 104 | configMaps = loadOption(cmd, "configMaps", options); 105 | kubernetesURL = loadOption(cmd, "kubernetesURL", options); 106 | jsPath = loadOption(cmd, "jsPath", options); 107 | 108 | String apiGroup = loadOption(cmd, "apiGroup", options); 109 | 110 | StringTokenizer toker = new StringTokenizer(apiGroup,",",false); 111 | List apiGroups = new ArrayList(); 112 | 113 | while (toker.hasMoreTokens()) { 114 | apiGroups.add(toker.nextToken()); 115 | } 116 | 117 | 118 | 119 | 120 | namespace = loadOption(cmd, "namespace", options); 121 | 122 | 123 | NetUtil.initialize(configMaps); 124 | 125 | String fromEnv = System.getenv(namespace); 126 | 127 | if (fromEnv != null) { 128 | namespace = fromEnv; 129 | } 130 | 131 | String objectType = loadOption(cmd, "objectType", options); 132 | 133 | 134 | 135 | 136 | Security.addProvider(new BouncyCastleProvider()); 137 | //ScriptEngine engine = initializeJS(jsPath, namespace, k8s); 138 | 139 | 140 | 141 | 142 | K8sUtils k8s = new K8sUtils(tokenPath, rootCaPath, configMaps, kubernetesURL); 143 | k8s.setEngine(null); 144 | 145 | 146 | runWatch(apiGroups, namespace, objectType, k8s); 147 | while (stillWatching) { 148 | Thread.sleep(1000); 149 | } 150 | 151 | for (RunWatch watch : watches) { 152 | watch.stopThread(); 153 | } 154 | // URL scriptURL = new URL(installScriptURL); 155 | // engine.eval(new BufferedReader(new 156 | // InputStreamReader(scriptURL.openStream()))); 157 | } 158 | } 159 | 160 | public static ScriptEngine initializeJS(String jsPath, String namespace, K8sUtils k8s) 161 | throws ScriptException, IOException, MalformedURLException { 162 | ScriptEngine engine = new ScriptEngineManager().getEngineByName("nashorn"); 163 | 164 | engine.getBindings(ScriptContext.ENGINE_SCOPE).put("k8s", k8s); 165 | engine.getBindings(ScriptContext.ENGINE_SCOPE).put("k8s_namespace", namespace); 166 | engine.getBindings(ScriptContext.ENGINE_SCOPE).put("js", engine); 167 | 168 | File[] scripts = new File(jsPath).listFiles(); 169 | for (File script : scripts) { 170 | if (script.getAbsolutePath().endsWith(".js")) { 171 | System.out.println("Loading Script : '" + script.getAbsolutePath() + "'"); 172 | engine.eval(new BufferedReader(new InputStreamReader(script.toURL().openStream()))); 173 | } 174 | } 175 | return engine; 176 | } 177 | 178 | private static void runWatch(List apiGroups, String namespace, String objectType, K8sUtils k8s) 179 | throws Exception, ParseException { 180 | 181 | for (String apiGroup : apiGroups) { 182 | String uri = "/apis/" + apiGroup + "/namespaces/" + namespace + "/" + objectType; 183 | RunWatch runWatch = new RunWatch(k8s,uri,"on_watch"); 184 | if (runWatch.isRightVersion()) { 185 | System.out.println("Using version '" + apiGroup + "'"); 186 | watches.add(runWatch); 187 | new Thread(runWatch).start(); 188 | break; 189 | } else { 190 | System.out.println("Unknown version '" + apiGroup + "'"); 191 | } 192 | } 193 | //uri = findResourceVersion(k8s, uri); 194 | //k8s.watchURI(uri,"on_watch"); 195 | 196 | 197 | 198 | } 199 | 200 | public static String findResourceVersion(K8sUtils k8s, String uri) throws Exception, ParseException { 201 | Map res = k8s.callWS(uri); 202 | String jsonObj = (String) res.get("data"); 203 | 204 | JSONObject root = (JSONObject) new JSONParser().parse(jsonObj); 205 | String resourceVersion = (String) ((JSONObject) root.get("metadata")).get("resourceVersion"); 206 | System.out.println(resourceVersion); 207 | 208 | //uri = uri + "?watch=true&resourceVersion=" + resourceVersion + "&fieldSelector=metadata.name=" + objectName; 209 | uri = uri + "?watch=true&resourceVersion=" + resourceVersion + "&timeoutSeconds=30"; 210 | return uri; 211 | } 212 | 213 | static String loadOption(CommandLine cmd,String name,Options options) { 214 | String val = cmd.getOptionValue(name); 215 | if (val == null) { 216 | System.err.println("Could not find option '" + name + "'"); 217 | HelpFormatter formatter = new HelpFormatter(); 218 | formatter.printHelp( "OpenUnison Kubernetes Artifact Deployer", options ); 219 | System.exit(1); 220 | return null; 221 | } else { 222 | return val; 223 | } 224 | } 225 | } -------------------------------------------------------------------------------- /src/main/java/com/tremolosecurity/kubernetes/artifacts/util/NetUtil.java: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Tremolo Security, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package com.tremolosecurity.kubernetes.artifacts.util; 16 | 17 | import java.io.BufferedReader; 18 | import java.io.File; 19 | import java.io.FileInputStream; 20 | import java.io.IOException; 21 | import java.io.InputStreamReader; 22 | import java.net.InetAddress; 23 | import java.net.MalformedURLException; 24 | import java.net.NetworkInterface; 25 | import java.net.SocketException; 26 | import java.net.URL; 27 | import java.net.URLConnection; 28 | import java.nio.charset.StandardCharsets; 29 | import java.nio.file.Files; 30 | import java.nio.file.Paths; 31 | import java.security.KeyManagementException; 32 | import java.security.KeyStore; 33 | import java.security.KeyStoreException; 34 | import java.security.NoSuchAlgorithmException; 35 | import java.security.UnrecoverableKeyException; 36 | import java.security.cert.X509Certificate; 37 | import java.util.ArrayList; 38 | import java.util.Enumeration; 39 | import java.util.UUID; 40 | import java.util.stream.Collectors; 41 | 42 | import javax.net.ssl.KeyManagerFactory; 43 | import javax.net.ssl.SSLContext; 44 | 45 | import com.tremolosecurity.kubernetes.artifacts.obj.HttpCon; 46 | 47 | import org.apache.http.Header; 48 | import org.apache.http.HttpResponse; 49 | import org.apache.http.HttpResponseFactory; 50 | import org.apache.http.client.config.CookieSpecs; 51 | import org.apache.http.client.config.RequestConfig; 52 | import org.apache.http.client.methods.HttpGet; 53 | import org.apache.http.config.Registry; 54 | import org.apache.http.config.RegistryBuilder; 55 | import org.apache.http.conn.socket.ConnectionSocketFactory; 56 | import org.apache.http.conn.socket.PlainConnectionSocketFactory; 57 | import org.apache.http.conn.ssl.SSLConnectionSocketFactory; 58 | import org.apache.http.conn.ssl.SSLContexts; 59 | import org.apache.http.impl.client.CloseableHttpClient; 60 | import org.apache.http.impl.client.HttpClients; 61 | import org.apache.http.impl.conn.BasicHttpClientConnectionManager; 62 | import org.apache.http.message.BasicHeader; 63 | 64 | /** 65 | * NetUtil 66 | */ 67 | public class NetUtil { 68 | 69 | private static KeyStore ks; 70 | private static String ksPassword; 71 | private static KeyManagerFactory kmf; 72 | private static Registry httpClientRegistry; 73 | private static RequestConfig globalHttpClientConfig; 74 | private static String pathToMoreCerts; 75 | 76 | public static void reinit() throws Exception { 77 | initialize(pathToMoreCerts); 78 | } 79 | 80 | public static void initialize(String pathToMoreCerts) throws Exception { 81 | NetUtil.pathToMoreCerts = pathToMoreCerts; 82 | ksPassword = UUID.randomUUID().toString(); 83 | ks = KeyStore.getInstance("PKCS12"); 84 | ks.load(null, ksPassword.toCharArray()); 85 | 86 | File moreCerts = new File(pathToMoreCerts); 87 | if (moreCerts.exists() && moreCerts.isDirectory()) { 88 | for (File certFile : moreCerts.listFiles()) { 89 | System.out.println("Processing - '" + certFile.getAbsolutePath() + "'"); 90 | if (certFile.isDirectory() || !certFile.getAbsolutePath().toLowerCase().endsWith(".pem")) { 91 | System.out.println("not a pem, sipping"); 92 | continue; 93 | } 94 | String certPem = new String(Files.readAllBytes(Paths.get(certFile.getAbsolutePath())), 95 | StandardCharsets.UTF_8); 96 | String alias = certFile.getName().substring(0, certFile.getName().indexOf('.')); 97 | 98 | CertUtils.importCertificate(ks, ksPassword, alias, certPem); 99 | } 100 | } 101 | 102 | KeyStore cacerts = KeyStore.getInstance(KeyStore.getDefaultType()); 103 | String cacertsPath = System.getProperty("javax.net.ssl.trustStore"); 104 | if (cacertsPath == null) { 105 | cacertsPath = System.getProperty("java.home") + "/lib/security/cacerts"; 106 | } 107 | 108 | cacerts.load(new FileInputStream(cacertsPath), null); 109 | 110 | Enumeration enumer = cacerts.aliases(); 111 | while (enumer.hasMoreElements()) { 112 | String alias = enumer.nextElement(); 113 | java.security.cert.Certificate cert = cacerts.getCertificate(alias); 114 | ks.setCertificateEntry(alias, cert); 115 | } 116 | 117 | initssl(); 118 | } 119 | 120 | public static void initssl() 121 | throws NoSuchAlgorithmException, KeyStoreException, UnrecoverableKeyException, KeyManagementException { 122 | kmf = KeyManagerFactory.getInstance("SunX509"); 123 | kmf.init(ks, ksPassword.toCharArray()); 124 | 125 | SSLContext sslctx = SSLContexts.custom().loadTrustMaterial(ks).loadKeyMaterial(ks, ksPassword.toCharArray()) 126 | .build(); 127 | SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(sslctx, 128 | SSLConnectionSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER); 129 | 130 | PlainConnectionSocketFactory sf = PlainConnectionSocketFactory.getSocketFactory(); 131 | httpClientRegistry = RegistryBuilder.create().register("http", sf) 132 | .register("https", sslsf).build(); 133 | 134 | globalHttpClientConfig = RequestConfig.custom().setCookieSpec(CookieSpecs.IGNORE_COOKIES) 135 | .setRedirectsEnabled(false).setAuthenticationEnabled(false).build(); 136 | } 137 | 138 | public static void addCertToStore(X509Certificate cert, String alias) throws KeyStoreException { 139 | ks.setCertificateEntry(alias, cert); 140 | } 141 | 142 | 143 | private static HttpCon createClient() throws Exception { 144 | ArrayList
defheaders = new ArrayList
(); 145 | defheaders.add(new BasicHeader("X-Csrf-Token", "1")); 146 | 147 | BasicHttpClientConnectionManager bhcm = new BasicHttpClientConnectionManager(httpClientRegistry); 148 | 149 | int numSecconds = 30; 150 | 151 | RequestConfig rc = RequestConfig.custom().setCookieSpec(CookieSpecs.STANDARD).setRedirectsEnabled(false) 152 | .setConnectTimeout(numSecconds * 1000) 153 | .setConnectionRequestTimeout(numSecconds * 1000) 154 | .setSocketTimeout(numSecconds * 1000).build(); 155 | 156 | CloseableHttpClient http = HttpClients.custom().setConnectionManager(bhcm).setDefaultHeaders(defheaders) 157 | .setDefaultRequestConfig(rc).build(); 158 | 159 | 160 | HttpCon con = new HttpCon(); 161 | con.setBcm(bhcm); 162 | con.setHttp(http); 163 | 164 | return con; 165 | 166 | } 167 | 168 | 169 | public static String downloadFile(String url) throws Exception { 170 | if (url.toLowerCase().startsWith("file://")) { 171 | return downloadFileFromFS(url); 172 | } else { 173 | return downloadFileFromWeb(url); 174 | } 175 | } 176 | 177 | private static String downloadFileFromFS(String url) throws Exception { 178 | 179 | URL urlObj = new URL(url); 180 | URLConnection conn = urlObj.openConnection(); 181 | String ret = null; 182 | try (BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8))) 183 | { 184 | ret = reader.lines().collect(Collectors.joining("\n")); 185 | } 186 | 187 | return ret; 188 | 189 | } 190 | 191 | /** 192 | * Downloads a file from the given URL 193 | * 194 | * @param url 195 | * @return 196 | * @throws IOException 197 | */ 198 | public static String downloadFileFromWeb(String url) throws Exception { 199 | HttpGet get = new HttpGet(url); 200 | HttpCon con = createClient(); 201 | 202 | try { 203 | HttpResponse resp = con.getHttp().execute(get); 204 | 205 | try (BufferedReader reader = new BufferedReader(new InputStreamReader(resp.getEntity().getContent(), StandardCharsets.UTF_8))) 206 | { 207 | return reader.lines().collect(Collectors.joining("\n")); 208 | } 209 | } finally { 210 | try { 211 | con.getHttp().close(); 212 | } catch (Exception e) { 213 | //do nothig 214 | } 215 | con.getBcm().shutdown(); 216 | con.getBcm().close(); 217 | } 218 | 219 | 220 | 221 | } 222 | 223 | /** 224 | * Determine your IP address 225 | * @return 226 | * @throws SocketException 227 | */ 228 | public static String whatsMyIP() throws SocketException { 229 | Enumeration enumer = NetworkInterface.getNetworkInterfaces(); 230 | while (enumer.hasMoreElements()) { 231 | NetworkInterface ni = enumer.nextElement(); 232 | Enumeration enumeri = ni.getInetAddresses(); 233 | while (enumeri.hasMoreElements()) { 234 | InetAddress addr = enumeri.nextElement(); 235 | if (! addr.getHostAddress().startsWith("127")) { 236 | return addr.getHostAddress(); 237 | } 238 | } 239 | } 240 | 241 | return ""; 242 | } 243 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /src/main/java/com/tremolosecurity/kubernetes/artifacts/util/K8sWatcher.java: -------------------------------------------------------------------------------- 1 | package com.tremolosecurity.kubernetes.artifacts.util; 2 | 3 | import java.io.BufferedReader; 4 | import java.io.IOException; 5 | import java.io.InputStreamReader; 6 | import java.io.UnsupportedEncodingException; 7 | import java.security.MessageDigest; 8 | import java.security.NoSuchAlgorithmException; 9 | import java.util.HashSet; 10 | import java.util.Map; 11 | 12 | import javax.script.Invocable; 13 | import javax.script.ScriptContext; 14 | import javax.script.ScriptEngine; 15 | 16 | import com.tremolosecurity.kubernetes.artifacts.obj.HttpCon; 17 | import com.tremolosecurity.kubernetes.artifacts.run.Controller; 18 | 19 | import org.apache.http.HttpResponse; 20 | import org.apache.http.client.ClientProtocolException; 21 | import org.apache.http.client.methods.CloseableHttpResponse; 22 | import org.apache.http.client.methods.HttpGet; 23 | import org.joda.time.DateTime; 24 | import org.joda.time.format.DateTimeFormat; 25 | import org.json.simple.JSONArray; 26 | import org.json.simple.JSONObject; 27 | import org.json.simple.parser.JSONParser; 28 | import org.json.simple.parser.ParseException; 29 | 30 | public class K8sWatcher { 31 | K8sUtils k8s; 32 | String lastProcessedResource; 33 | HashSet processedVersions; 34 | String functionName; 35 | HashSet expired; 36 | 37 | String lastResourceId; 38 | long lastResourceIdNum; 39 | 40 | public K8sWatcher(K8sUtils k8s, String functionName) { 41 | this.k8s = k8s; 42 | this.processedVersions = new HashSet(); 43 | this.functionName = functionName; 44 | this.expired = new HashSet(); 45 | } 46 | 47 | public boolean isValidUri(String uri) { 48 | // load up the existing objects 49 | Map resp; 50 | try { 51 | resp = k8s.callWS(uri); 52 | int responseCode = (int) resp.get("code"); 53 | return (responseCode == 200); 54 | } catch (Exception e) { 55 | e.printStackTrace(); 56 | return false; 57 | } 58 | 59 | } 60 | 61 | public void watchUri(String uri) throws Exception { 62 | processExistingObjects(uri); 63 | 64 | JSONParser parser = new JSONParser(); 65 | HttpCon http = null; 66 | 67 | try { 68 | http = this.k8s.createClient(); 69 | 70 | boolean keepRunning = true; 71 | 72 | while (keepRunning) { 73 | StringBuilder sb = new StringBuilder().append(k8s.getK8sUrl()).append(uri) 74 | .append("?watch=true&timeoutSeconds=30&allowWatchBookmarks=true"); 75 | if (this.lastProcessedResource != null) { 76 | sb.append("&resourceVersion=").append(this.lastProcessedResource); 77 | } 78 | String url = sb.toString(); 79 | System.out.println(url); 80 | HttpGet watchApi = new HttpGet(url); 81 | String token = this.k8s.getAuthorizationToken(); 82 | if (token != null) { 83 | watchApi.addHeader("Authorization", "Bearer " + token); 84 | } 85 | 86 | CloseableHttpResponse resp = null; 87 | 88 | try { 89 | resp = http.getHttp().execute(watchApi); 90 | } catch (IOException e) { 91 | System.out.println("Unable to contact api server"); 92 | e.printStackTrace(); 93 | System.out.println("Sleeping for 2 seconds..."); 94 | Thread.sleep(2000); 95 | System.out.println("Trying again"); 96 | continue; 97 | 98 | } 99 | 100 | BufferedReader in = new BufferedReader(new InputStreamReader(resp.getEntity().getContent())); 101 | 102 | String line = null; 103 | while ((line = in.readLine()) != null) { 104 | JSONObject event = (JSONObject) parser.parse(line); 105 | JSONObject object = (JSONObject) event.get("object"); 106 | 107 | if (event.get("kind") != null && event.get("kind").equals("BOOKMARK")) { 108 | this.lastResourceId = getResourceVersion(object ); 109 | this.processedVersions.add(this.lastResourceId); 110 | continue; 111 | } else if (event.get("kind") != null && event.get("kind").equals("ERROR")) { 112 | // there was an error 113 | long errorCode = (Long) event.get("code"); 114 | 115 | if (errorCode == 504 || errorCode == 410) { 116 | String msg = (String) object.get("message"); 117 | int indexstart = msg.indexOf('('); 118 | if (indexstart == -1) { 119 | //i'm not really sure how to handle this 120 | throw new Exception(String.format("Could not process watch %s",msg)); 121 | } else { 122 | int indexend = msg.indexOf(')'); 123 | String newResourceId = msg.substring(indexstart+1,indexend); 124 | this.lastResourceId = newResourceId; 125 | this.processedVersions.add(newResourceId); 126 | continue; 127 | } 128 | } 129 | } 130 | 131 | if (object.get("kind") != null && object.get("kind").equals("Status")) { 132 | if (object.get("status").equals("Failure")) { 133 | System.out.println("Watch failed : " + line); 134 | if(object.get("reason").equals("Expired")) { 135 | this.expired.add(this.lastProcessedResource); 136 | this.lastProcessedResource = null; 137 | break; 138 | } 139 | } 140 | } 141 | 142 | String resourceVersion = getResourceVersion(object ); 143 | 144 | if (resourceVersion == null) { 145 | 146 | throw new Exception("No resource " + line); 147 | 148 | } else { 149 | if (this.processedVersions.contains(resourceVersion)) { 150 | System.out 151 | .println("Resource " + resourceVersion + " has already been processed, skipping"); 152 | } else { 153 | if (! this.processedVersions.contains(resourceVersion)) { 154 | this.processedVersions.add(resourceVersion); 155 | } 156 | 157 | if (! this.expired.contains(resourceVersion)) { 158 | this.lastProcessedResource = resourceVersion; 159 | } else { 160 | this.lastProcessedResource = null; 161 | } 162 | 163 | 164 | String eventType = (String) event.get("type"); 165 | if (eventType.equalsIgnoreCase("MODIFIED")) { 166 | if (hasObjectChanged((JSONObject) event.get("object"))) { 167 | 168 | // process the object 169 | try { 170 | processEvent(event,uri); 171 | } catch (Exception e) { 172 | e.printStackTrace(); 173 | } 174 | } else { 175 | System.out.println("No change, skipping"); 176 | } 177 | } else { 178 | // process the object 179 | try { 180 | processEvent(event,uri); 181 | } catch (Exception e) { 182 | e.printStackTrace(); 183 | } 184 | } 185 | } 186 | } 187 | 188 | } 189 | 190 | resp.close(); 191 | watchApi.abort(); 192 | 193 | } 194 | 195 | } catch (Exception e) { 196 | 197 | e.printStackTrace(); 198 | } finally { 199 | if (http != null) { 200 | try { 201 | http.getHttp().close(); 202 | } catch (Exception e) { 203 | // do nothing 204 | } 205 | http.getBcm().close(); 206 | } 207 | } 208 | 209 | } 210 | 211 | private void processExistingObjects(String uri) throws Exception { 212 | 213 | // load up the existing objects 214 | Map resp = k8s.callWS(uri); 215 | int responseCode = (int) resp.get("code"); 216 | if (responseCode != 200) { 217 | throw new Exception("Unable to load " + uri + " - " + resp); 218 | } 219 | 220 | JSONParser parser = new JSONParser(); 221 | JSONObject root = (JSONObject) parser.parse((String) resp.get("data")); 222 | JSONArray items = (JSONArray) root.get("items"); 223 | for (Object o : items) { 224 | JSONObject item = (JSONObject) o; 225 | 226 | String resourceVersion = this.getResourceVersion(item); 227 | if (resourceVersion == null) { 228 | System.out.println("skipping " + item); 229 | continue; 230 | } 231 | 232 | this.processedVersions.add(resourceVersion); 233 | this.lastProcessedResource = resourceVersion; 234 | if (hasStatus(item)) { 235 | 236 | // has been processed at least once 237 | if (hasObjectChanged(item)) { 238 | // The object has been updated since the operator was last run 239 | JSONObject change = new JSONObject(); 240 | change.put("object", item); 241 | change.put("type", "MODIFIED"); 242 | 243 | // process the object 244 | try { 245 | processEvent(change,uri); 246 | } catch (Exception e) { 247 | e.printStackTrace(); 248 | } 249 | 250 | } 251 | 252 | } else { 253 | // no status, so it needs to be added 254 | JSONObject add = new JSONObject(); 255 | add.put("object", item); 256 | add.put("type", "ADDED"); 257 | 258 | // process the object 259 | try { 260 | processEvent(add,uri); 261 | } catch (Exception e) { 262 | e.printStackTrace(); 263 | } 264 | 265 | } 266 | } 267 | 268 | } 269 | 270 | public static boolean hasStatus(JSONObject object) { 271 | return object.get("status") != null; 272 | } 273 | 274 | public static boolean hasObjectChanged(JSONObject cr) 275 | throws ParseException, NoSuchAlgorithmException, UnsupportedEncodingException { 276 | if (!hasStatus(cr)) { 277 | // no status, nothing to compare against so it has changed 278 | return true; 279 | } 280 | 281 | JSONObject chkObj = generateCleanCR(cr); 282 | 283 | String digestBase64 = generateCheckSum(chkObj); 284 | 285 | String existingDigest = (String) ((JSONObject) cr.get("status")).get("digest"); 286 | return existingDigest == null || !existingDigest.equals(digestBase64); 287 | } 288 | 289 | private static String generateCheckSum(JSONObject chkObj) 290 | throws NoSuchAlgorithmException, UnsupportedEncodingException { 291 | String jsonForChecksum = chkObj.toJSONString(); 292 | MessageDigest digest = java.security.MessageDigest.getInstance("SHA-256"); 293 | digest.update(jsonForChecksum.getBytes("UTF-8"), 0, jsonForChecksum.getBytes("UTF-8").length); 294 | byte[] digestBytes = digest.digest(); 295 | String digestBase64 = java.util.Base64.getEncoder().encodeToString(digestBytes); 296 | return digestBase64; 297 | } 298 | 299 | private static JSONObject generateCleanCR(JSONObject cr) throws ParseException { 300 | JSONParser parser = new JSONParser(); 301 | 302 | JSONObject chkObj = new JSONObject(); 303 | chkObj.put("apiVersion", cr.get("apiVersion")); 304 | chkObj.put("kind", cr.get("kind")); 305 | chkObj.put("spec", cr.get("spec")); 306 | 307 | JSONObject metadata = (JSONObject) parser.parse(((JSONObject) cr.get("metadata")).toJSONString()); 308 | 309 | metadata.remove("creationTimestamp"); 310 | metadata.remove("generation"); 311 | metadata.remove("resourceVersion"); 312 | metadata.remove("managedFields"); 313 | 314 | chkObj.put("metadata", metadata); 315 | return chkObj; 316 | } 317 | 318 | private String getResourceVersion(JSONObject cr) { 319 | 320 | JSONObject metadata = (JSONObject) (JSONObject) cr.get("metadata"); 321 | 322 | String resourceVersion = (String) metadata.get("resourceVersion"); 323 | 324 | if (resourceVersion == null) { 325 | System.out.println("unexpected json - " + cr.toString()); 326 | return null; 327 | } else { 328 | return resourceVersion; 329 | } 330 | } 331 | 332 | private String processEvent(JSONObject event,String uri) throws Exception { 333 | K8sUtils localK8s = new K8sUtils(Controller.tokenPath, Controller.rootCaPath, Controller.configMaps, 334 | Controller.kubernetesURL); 335 | 336 | String selfUri = uri; 337 | if (selfUri.contains("?")) { 338 | selfUri = selfUri.substring(0,selfUri.indexOf('?')); 339 | } 340 | 341 | 342 | JSONObject obj = (JSONObject) event.get("object"); 343 | 344 | selfUri += "/" + ((JSONObject)obj.get("metadata")).get("name"); 345 | 346 | ScriptEngine localEngine = Controller.initializeJS(Controller.jsPath, Controller.namespace, localK8s); 347 | localEngine.getBindings(ScriptContext.ENGINE_SCOPE).put("selfLink", selfUri); 348 | localK8s.setEngine(localEngine); 349 | 350 | Invocable invocable = (Invocable) localEngine; 351 | 352 | boolean error = false; 353 | 354 | String result = null; 355 | 356 | try { 357 | System.out.println("Invoking javascript"); 358 | result = (String) invocable.invokeFunction(functionName, event.toString()); 359 | System.out.println("Done invoking javascript"); 360 | } catch (Throwable t) { 361 | System.err.println("Error on watch - " + event.toString()); 362 | t.printStackTrace(System.err); 363 | error = true; 364 | } 365 | 366 | if (error) { 367 | if (result != null) { 368 | result = "error-" + result; 369 | } else { 370 | result = "error"; 371 | } 372 | } 373 | 374 | 375 | System.out.println("Checking if need to create a status for : '" + event.get("type") + "'"); 376 | if (event.get("type").equals("MODIFIED") || event.get("type").equals("ADDED")) { 377 | System.out.println("Generating status"); 378 | JSONObject cr = generateCleanCR((JSONObject) event.get("object")); 379 | 380 | String digestBase64 = generateCheckSum(cr); 381 | ((JSONObject) cr.get("metadata")).put("resourceVersion", 382 | (String) ((JSONObject) ((JSONObject) event.get("object")).get("metadata")).get("resourceVersion")); 383 | JSONObject patch = generateJsonStatus(result, digestBase64, localK8s.getAdditionalStatuses()); 384 | System.out.println("Creating status patch : " + patch); 385 | 386 | 387 | 388 | selfUri += "/status"; 389 | System.out.println("Patching to '" + selfUri + "'"); 390 | 391 | JSONObject status = new JSONObject(); 392 | status.put("status", patch); 393 | //String selfLink = (String) ((JSONObject) ((JSONObject) event.get("object")).get("metadata")) 394 | // .get("selfLink"); 395 | 396 | 397 | System.out.println("Patch : '" + status.toString() + "'"); 398 | System.out.println(this.k8s.patchWS(selfUri, status.toString())); 399 | 400 | } 401 | 402 | return result; 403 | 404 | } 405 | 406 | private JSONObject generateJsonStatus(String errorMessage, String digest, Map additionalStatuses) { 407 | JSONObject patch = new JSONObject(); 408 | 409 | patch.put("conditions", new JSONObject()); 410 | ((JSONObject) patch.get("conditions")).put("lastTransitionTime", 411 | DateTimeFormat.forPattern("yyyy-MM-dd hh:mm:ssz").print(new DateTime())); 412 | patch.put("digest", digest); 413 | 414 | if (errorMessage == null) { 415 | ((JSONObject) patch.get("conditions")).put("status", "True"); 416 | ((JSONObject) patch.get("conditions")).put("type", "Completed"); 417 | } else { 418 | ((JSONObject) patch.get("conditions")).put("status", "True"); 419 | ((JSONObject) patch.get("conditions")).put("type", "Failed"); 420 | ((JSONObject) patch.get("conditions")).put("status", "True"); 421 | ((JSONObject) patch.get("conditions")).put("reason", errorMessage); 422 | } 423 | 424 | for (String extraStatus : additionalStatuses.keySet()) { 425 | this.addToJson(patch, extraStatus, additionalStatuses.get(extraStatus)); 426 | } 427 | 428 | return patch; 429 | } 430 | 431 | private void addToJson(JSONObject root, String name, Object value) { 432 | if (value instanceof String) { 433 | root.put(name, value); 434 | } else if (value instanceof Map) { 435 | JSONObject newRoot = new JSONObject(); 436 | root.put(name, newRoot); 437 | Map rootSet = (Map) value; 438 | for (String keyName : rootSet.keySet()) { 439 | addToJson(newRoot, keyName, rootSet.get(keyName)); 440 | } 441 | } else { 442 | System.out.println("Unknown type" + value); 443 | } 444 | } 445 | } 446 | -------------------------------------------------------------------------------- /src/main/java/com/tremolosecurity/kubernetes/artifacts/util/CertUtils.java: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Tremolo Security, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package com.tremolosecurity.kubernetes.artifacts.util; 16 | 17 | import java.io.ByteArrayInputStream; 18 | import java.io.ByteArrayOutputStream; 19 | import java.io.FileInputStream; 20 | import java.io.FileNotFoundException; 21 | import java.io.IOException; 22 | import java.io.PrintWriter; 23 | import java.io.UnsupportedEncodingException; 24 | import java.math.BigInteger; 25 | 26 | import java.security.InvalidKeyException; 27 | import java.security.KeyFactory; 28 | import java.security.KeyPair; 29 | import java.security.KeyPairGenerator; 30 | import java.security.KeyStore; 31 | import java.security.KeyStoreException; 32 | import java.security.NoSuchAlgorithmException; 33 | import java.security.NoSuchProviderException; 34 | import java.security.PrivateKey; 35 | import java.security.SecureRandom; 36 | import java.security.SignatureException; 37 | import java.security.UnrecoverableKeyException; 38 | import java.security.cert.Certificate; 39 | import java.security.cert.CertificateException; 40 | import java.security.cert.CertificateFactory; 41 | import java.security.cert.X509Certificate; 42 | import java.security.spec.InvalidKeySpecException; 43 | import java.security.spec.PKCS8EncodedKeySpec; 44 | import java.time.Instant; 45 | import java.time.temporal.ChronoUnit; 46 | import java.util.Arrays; 47 | import java.util.Collection; 48 | import java.util.Date; 49 | import java.util.Enumeration; 50 | import java.util.HashMap; 51 | import java.util.HashSet; 52 | import java.util.Iterator; 53 | import java.util.Map; 54 | 55 | import javax.crypto.KeyGenerator; 56 | import javax.crypto.SecretKey; 57 | import javax.crypto.SecretKeyFactory; 58 | import javax.crypto.spec.SecretKeySpec; 59 | import javax.security.auth.x500.X500Principal; 60 | 61 | import com.fasterxml.jackson.databind.DeserializationFeature; 62 | import com.fasterxml.jackson.databind.ObjectMapper; 63 | import com.tremolosecurity.kubernetes.artifacts.obj.CertificateData; 64 | import com.tremolosecurity.kubernetes.artifacts.obj.X509Data; 65 | 66 | import org.apache.commons.codec.binary.Base64; 67 | import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers; 68 | import org.bouncycastle.asn1.pkcs.PrivateKeyInfo; 69 | import org.bouncycastle.asn1.x500.X500Name; 70 | import org.bouncycastle.asn1.x509.BasicConstraints; 71 | import org.bouncycastle.asn1.x509.ExtendedKeyUsage; 72 | import org.bouncycastle.asn1.x509.Extension; 73 | import org.bouncycastle.asn1.x509.ExtensionsGenerator; 74 | import org.bouncycastle.asn1.x509.GeneralName; 75 | import org.bouncycastle.asn1.x509.GeneralNames; 76 | import org.bouncycastle.asn1.x509.KeyPurposeId; 77 | import org.bouncycastle.asn1.x509.KeyUsage; 78 | import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo; 79 | import org.bouncycastle.asn1.x509.X509Extensions; 80 | import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; 81 | import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder; 82 | import org.bouncycastle.crypto.util.PrivateKeyFactory; 83 | import org.bouncycastle.crypto.util.PrivateKeyInfoFactory; 84 | import org.bouncycastle.jce.PKCS10CertificationRequest; 85 | import org.bouncycastle.jce.provider.BouncyCastleProvider; 86 | import org.bouncycastle.openssl.PKCS8Generator; 87 | import org.bouncycastle.operator.ContentSigner; 88 | import org.bouncycastle.operator.OperatorCreationException; 89 | import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; 90 | import org.bouncycastle.pkcs.PKCS10CertificationRequestBuilder; 91 | import org.bouncycastle.util.io.pem.PemWriter; 92 | import org.bouncycastle.x509.X509V3CertificateGenerator; 93 | import org.joda.time.DateTime; 94 | 95 | /** 96 | * Static utility class meant for being called from within javascript 97 | */ 98 | public class CertUtils { 99 | static SecureRandom secRandom = new SecureRandom(); 100 | 101 | /** 102 | * Generates an AES-256 SecretKey 103 | * 104 | * @return 105 | * @throws Exception 106 | */ 107 | public static void createKey(KeyStore ks, String alias, String ksPassword) throws Exception { 108 | KeyGenerator kg = KeyGenerator.getInstance("AES"); 109 | kg.init(256, secRandom); 110 | SecretKey sk = kg.generateKey(); 111 | ks.setKeyEntry(alias, sk, ksPassword.toCharArray(), null); 112 | } 113 | 114 | /** 115 | * Exports a key to a base64 encoded string 116 | */ 117 | public static String exportKey(KeyStore ks, String alias, String ksPassword) throws Exception { 118 | SecretKey key = (SecretKey) ks.getKey(alias, ksPassword.toCharArray()); 119 | return java.util.Base64.getEncoder().encodeToString(key.getEncoded()); 120 | } 121 | 122 | /** 123 | * Stores the key in the keystore 124 | */ 125 | public static void storeKey(KeyStore ks, String alias, String ksPassword, String encodedKey) 126 | throws KeyStoreException { 127 | byte[] rawKey = java.util.Base64.getDecoder().decode(encodedKey); 128 | SecretKey sc = new SecretKeySpec(rawKey, "AES"); 129 | ks.setKeyEntry(alias, sc, ksPassword.toCharArray(), null); 130 | } 131 | 132 | /** 133 | * Create an X509Data object from a JSON version from JavaScript 134 | * 135 | * @param fromjs 136 | * @return 137 | * @throws Exception 138 | */ 139 | public static X509Data createCertificate(Map fromjs) throws Exception { 140 | CertificateData data = new ObjectMapper().convertValue(fromjs, CertificateData.class); 141 | 142 | return createCertificate(data); 143 | } 144 | 145 | /** 146 | * Create a certificate and keypair based on a certificate data object 147 | * 148 | * @param certData 149 | * @return 150 | * @throws Exception 151 | */ 152 | public static X509Data createCertificate(CertificateData certData) throws Exception { 153 | String keyAlg = "RSA"; 154 | 155 | KeyPairGenerator kpg = KeyPairGenerator.getInstance(keyAlg); 156 | kpg.initialize(certData.getSize(), secRandom); 157 | KeyPair kp = kpg.generateKeyPair(); 158 | 159 | X500Name dnName = new X500Name("CN=" + certData.getServerName() + ", OU=" + certData.getOu() + ", O=" 160 | + certData.getO() + ", L=" + certData.getL() + ", ST=" + certData.getSt() + ", C=" + certData.getC()); 161 | 162 | BigInteger certSerialNumber = BigInteger.valueOf(System.currentTimeMillis()); 163 | 164 | ContentSigner contentSigner = new JcaContentSignerBuilder(certData.getSigAlg()).build(kp.getPrivate()); 165 | 166 | Instant startDate = Instant.ofEpochMilli(certData.getNotBefore().getTime()); 167 | Instant endDate = Instant.ofEpochMilli(certData.getNotAfter().getTime()); 168 | 169 | 170 | JcaX509v3CertificateBuilder certBuilder = new JcaX509v3CertificateBuilder( 171 | dnName, certSerialNumber, Date.from(startDate), Date.from(endDate), dnName, 172 | kp.getPublic()); 173 | 174 | if (certData.isCaCert()) { 175 | //certBuilder.addExtension(Extension.create(Extension.basicConstraints, true, new BasicConstraints(true))); 176 | certBuilder.addExtension(Extension.create(Extension.keyUsage, true, new KeyUsage(KeyUsage.keyCertSign))); 177 | certBuilder.addExtension(Extension.create(Extension.extendedKeyUsage, true, new ExtendedKeyUsage(KeyPurposeId.anyExtendedKeyUsage))); 178 | } else { 179 | certBuilder.addExtension(Extension.create(Extension.extendedKeyUsage, true, new ExtendedKeyUsage(KeyPurposeId.id_kp_serverAuth))); 180 | certBuilder.addExtension(Extension.create(Extension.keyUsage, true, new KeyUsage(KeyUsage.keyEncipherment | KeyUsage.digitalSignature))); 181 | } 182 | 183 | GeneralName[] names = new GeneralName[certData.getSubjectAlternativeNames().size() + 1]; 184 | names[0] = new GeneralName(GeneralName.dNSName, certData.getServerName()); 185 | for (int i = 0; i < certData.getSubjectAlternativeNames().size(); i++) { 186 | names[i + 1] = new GeneralName(GeneralName.dNSName, certData.getSubjectAlternativeNames().get(i)); 187 | } 188 | 189 | GeneralNames subjectAltName = new GeneralNames(names); 190 | certBuilder.addExtension(Extension.subjectAlternativeName,false,subjectAltName ); 191 | 192 | 193 | Certificate certificate = new JcaX509CertificateConverter().setProvider(BouncyCastleProvider.PROVIDER_NAME) 194 | .getCertificate(certBuilder.build(contentSigner)); 195 | 196 | /*X509V3CertificateGenerator certGen = new X509V3CertificateGenerator(); 197 | 198 | certGen.setSerialNumber(BigInteger.valueOf(System.currentTimeMillis())); 199 | certGen.setIssuerDN(new X500Principal("CN=" + certData.getServerName() + ", OU=" + certData.getOu() + ", O=" 200 | + certData.getO() + ", L=" + certData.getL() + ", ST=" + certData.getSt() + ", C=" + certData.getC())); 201 | certGen.setNotBefore(certData.getNotBefore()); 202 | certGen.setNotAfter(certData.getNotAfter()); 203 | certGen.setSubjectDN(new X500Principal("CN=" + certData.getServerName() + ", OU=" + certData.getOu() + ", O=" 204 | + certData.getO() + ", L=" + certData.getL() + ", ST=" + certData.getSt() + ", C=" + certData.getC())); 205 | certGen.setPublicKey(kp.getPublic()); 206 | certGen.setSignatureAlgorithm(certData.getSigAlg()); 207 | 208 | if (certData.isCaCert()) { 209 | certGen.addExtension(X509Extensions.BasicConstraints, true, new BasicConstraints(true)); 210 | 211 | certGen.addExtension(X509Extensions.KeyUsage, true, new KeyUsage(KeyUsage.keyCertSign)); 212 | 213 | certGen.addExtension(X509Extensions.ExtendedKeyUsage, true, 214 | new ExtendedKeyUsage(KeyPurposeId.anyExtendedKeyUsage)); 215 | } 216 | 217 | GeneralName[] names = new GeneralName[certData.getSubjectAlternativeNames().size() + 1]; 218 | names[0] = new GeneralName(GeneralName.dNSName, certData.getServerName()); 219 | for (int i = 0; i < certData.getSubjectAlternativeNames().size(); i++) { 220 | names[i + 1] = new GeneralName(GeneralName.dNSName, certData.getSubjectAlternativeNames().get(i)); 221 | } 222 | 223 | GeneralNames subjectAltName = new GeneralNames(names); 224 | 225 | certGen.addExtension(X509Extensions.SubjectAlternativeName, false, subjectAltName); 226 | 227 | X509Certificate cert = certGen.generate(kp.getPrivate(), secRandom);*/ 228 | 229 | return new X509Data(kp, (X509Certificate) certificate, certData); 230 | 231 | } 232 | 233 | /** 234 | * Generate a CSR based on X509 235 | * 236 | * @param x509 237 | * @return 238 | * @throws InvalidKeyException 239 | * @throws NoSuchAlgorithmException 240 | * @throws NoSuchProviderException 241 | * @throws SignatureException 242 | * @throws IOException 243 | * @throws OperatorCreationException 244 | */ 245 | public static String generateCSR(X509Data x509) throws InvalidKeyException, NoSuchAlgorithmException, 246 | NoSuchProviderException, SignatureException, IOException, OperatorCreationException { 247 | PKCS10CertificationRequestBuilder kpGen = new PKCS10CertificationRequestBuilder( 248 | new org.bouncycastle.asn1.x500.X500Name(x509.getCertificate().getSubjectX500Principal().getName()), 249 | SubjectPublicKeyInfo.getInstance(x509.getKeyData().getPublic().getEncoded())); 250 | 251 | GeneralName[] sans = new GeneralName[x509.getCertInput().getSubjectAlternativeNames().size() + 1]; 252 | sans[0] = new GeneralName(GeneralName.dNSName, x509.getCertInput().getServerName()); 253 | for (int i = 0; i < x509.getCertInput().getSubjectAlternativeNames().size(); i++) { 254 | sans[i + 1] = new GeneralName(GeneralName.dNSName, x509.getCertInput().getSubjectAlternativeNames().get(i)); 255 | } 256 | 257 | GeneralNames subjectAltName = new GeneralNames(sans); 258 | ExtensionsGenerator extGen = new ExtensionsGenerator(); 259 | extGen.addExtension(Extension.subjectAlternativeName, false, subjectAltName.toASN1Primitive()); 260 | 261 | kpGen.addAttribute(PKCSObjectIdentifiers.pkcs_9_at_extensionRequest, extGen.generate()); 262 | 263 | JcaContentSignerBuilder csBuilder = new JcaContentSignerBuilder(x509.getCertInput().getSigAlg()); 264 | ContentSigner signer = csBuilder.build(x509.getKeyData().getPrivate()); 265 | org.bouncycastle.pkcs.PKCS10CertificationRequest request = kpGen.build(signer); 266 | 267 | Base64 encoder = new Base64(67); 268 | String b64 = encoder.encodeToString(request.getEncoded()).trim(); 269 | b64 = "-----BEGIN CERTIFICATE REQUEST-----\n" + b64 + "\n-----END CERTIFICATE REQUEST-----\n"; 270 | 271 | return b64; 272 | } 273 | 274 | /** 275 | * Parse a PEM file into a certificate 276 | * 277 | * @param b64Cert 278 | * @return 279 | * @throws Exception 280 | */ 281 | public static X509Certificate string2cert(String b64Cert) throws Exception { 282 | // System.out.println(b64Cert); 283 | // System.out.println(""); 284 | b64Cert = b64Cert.replace("\n", ""); 285 | // System.out.println(b64Cert); 286 | ByteArrayInputStream bais = new ByteArrayInputStream(Base64.decodeBase64(b64Cert)); 287 | CertificateFactory cf = CertificateFactory.getInstance("X.509"); 288 | Collection c = cf.generateCertificates(bais); 289 | return (X509Certificate) c.iterator().next(); 290 | } 291 | 292 | public static X509Certificate pem2cert(String pem) throws Exception { 293 | if (!pem.startsWith("-")) { 294 | pem = new String(java.util.Base64.getDecoder().decode(pem)); 295 | } 296 | 297 | ByteArrayInputStream bais = new ByteArrayInputStream(pem.getBytes("UTF-8")); 298 | CertificateFactory cf = CertificateFactory.getInstance("X.509"); 299 | Collection c = cf.generateCertificates(bais); 300 | return (X509Certificate) c.iterator().next(); 301 | } 302 | 303 | /** 304 | * Generate a PEM from a certificate 305 | * 306 | * @param cert 307 | * @return 308 | * @throws Exception 309 | */ 310 | public static String exportCert(X509Certificate cert) throws Exception { 311 | 312 | Base64 encoder = new Base64(64); 313 | 314 | String b64 = encoder.encodeToString(cert.getEncoded()); 315 | 316 | b64 = "-----BEGIN CERTIFICATE-----\n" + b64 + "-----END CERTIFICATE-----\n"; 317 | 318 | return b64; 319 | } 320 | 321 | /** 322 | * Expot and base64 encode a private key 323 | * 324 | * @param pk 325 | * @return 326 | * @throws Exception 327 | */ 328 | public static String exportKey(PrivateKey pk) throws Exception { 329 | 330 | ByteArrayOutputStream baos = new ByteArrayOutputStream(); 331 | PrintWriter out = new PrintWriter(baos); 332 | 333 | PemWriter pem = new PemWriter(out); 334 | pem.writeObject(new PKCS8Generator(PrivateKeyInfo.getInstance(pk.getEncoded()), null)); 335 | pem.close(); 336 | out.flush(); 337 | out.close(); 338 | 339 | return new String(baos.toByteArray()); 340 | } 341 | 342 | /** 343 | * Import a signed certificate into the keystore for an existing keypair 344 | * 345 | * @param x509 346 | * @param b64cert 347 | * @throws CertificateException 348 | * @throws UnsupportedEncodingException 349 | * @throws KeyStoreException 350 | */ 351 | public static void importSignedCert(X509Data x509, String b64cert) 352 | throws CertificateException, UnsupportedEncodingException, KeyStoreException { 353 | String pemCert = new String(java.util.Base64.getDecoder().decode(b64cert)); 354 | ByteArrayInputStream bais = new ByteArrayInputStream(pemCert.getBytes("UTF-8")); 355 | CertificateFactory cf = CertificateFactory.getInstance("X.509"); 356 | Collection c = cf.generateCertificates(bais); 357 | x509.setCertificate((X509Certificate) c.iterator().next()); 358 | } 359 | 360 | /** 361 | * Import a PEM encoded certificate into the keystore 362 | * 363 | * @param ks 364 | * @param ksPass 365 | * @param alias 366 | * @param pemCert 367 | * @throws CertificateException 368 | * @throws UnsupportedEncodingException 369 | * @throws KeyStoreException 370 | */ 371 | public static void importCertificate(KeyStore ks, String ksPass, String alias, String pemCert) 372 | throws CertificateException, UnsupportedEncodingException, KeyStoreException { 373 | Collection c = pem2certs(pemCert); 374 | 375 | if (c.size() > 1) { 376 | int j = 0; 377 | Iterator i = c.iterator(); 378 | while (i.hasNext()) { 379 | Certificate certificate = (Certificate) i.next(); 380 | if (j == 0) { 381 | ks.setCertificateEntry(alias, certificate); 382 | } else { 383 | ks.setCertificateEntry(alias + "-" + j, certificate); 384 | } 385 | j++; 386 | } 387 | } else { 388 | ks.setCertificateEntry(alias, c.iterator().next()); 389 | } 390 | 391 | } 392 | 393 | private static Collection pem2certs(String pemCert) 394 | throws UnsupportedEncodingException, CertificateException { 395 | 396 | if (!pemCert.startsWith("-")) { 397 | pemCert = new String(java.util.Base64.getDecoder().decode(pemCert)); 398 | } 399 | 400 | ByteArrayInputStream bais = new ByteArrayInputStream(pemCert.getBytes("UTF-8")); 401 | CertificateFactory cf = CertificateFactory.getInstance("X.509"); 402 | Collection c = cf.generateCertificates(bais); 403 | return c; 404 | } 405 | 406 | /** 407 | * Import a certificate into the keystore 408 | * 409 | * @param ks 410 | * @param ksPass 411 | * @param alias 412 | * @param cert 413 | * @throws CertificateException 414 | * @throws UnsupportedEncodingException 415 | * @throws KeyStoreException 416 | */ 417 | public static void importCertificate(KeyStore ks, String ksPass, String alias, X509Certificate cert) 418 | throws CertificateException, UnsupportedEncodingException, KeyStoreException { 419 | 420 | ks.setCertificateEntry(alias, cert); 421 | 422 | } 423 | 424 | /** 425 | * Save full X509 data key into the keystore 426 | * 427 | * @param ks 428 | * @param ksPass 429 | * @param alias 430 | * @param x509 431 | * @throws KeyStoreException 432 | */ 433 | public static void saveX509ToKeystore(KeyStore ks, String ksPass, String alias, X509Data x509) 434 | throws KeyStoreException { 435 | ks.setKeyEntry(alias, x509.getKeyData().getPrivate(), ksPass.toCharArray(), 436 | new Certificate[] { x509.getCertificate() }); 437 | } 438 | 439 | /** 440 | * Base64 Encode the keystore 441 | * 442 | * @param ks 443 | * @param ksPassword 444 | * @return 445 | * @throws KeyStoreException 446 | * @throws NoSuchAlgorithmException 447 | * @throws CertificateException 448 | * @throws IOException 449 | */ 450 | public static String encodeKeyStore(KeyStore ks, String ksPassword) 451 | throws KeyStoreException, NoSuchAlgorithmException, CertificateException, IOException { 452 | 453 | ByteArrayOutputStream baos = new ByteArrayOutputStream(); 454 | ks.store(baos, ksPassword.toCharArray()); 455 | return java.util.Base64.getEncoder().encodeToString(baos.toByteArray()); 456 | } 457 | 458 | public static void importKeyPairAndCert(KeyStore ks, String ksPass, String alias, String privateKeyEncoded, 459 | String certEncoded) throws IOException, CertificateException, KeyStoreException, NoSuchAlgorithmException, 460 | InvalidKeySpecException { 461 | 462 | String privateKeyPEM = new String(java.util.Base64.getDecoder().decode(privateKeyEncoded)); 463 | 464 | privateKeyPEM = privateKeyPEM.replace("-----BEGIN PRIVATE KEY-----", "").replace("-----END PRIVATE KEY-----", 465 | "");// .trim();//.replaceAll("[\n,\r]", "").trim(); 466 | byte[] pkBytes = org.bouncycastle.util.encoders.Base64.decode(privateKeyPEM); 467 | PKCS8EncodedKeySpec kspec = new PKCS8EncodedKeySpec(pkBytes); 468 | KeyFactory kf = KeyFactory.getInstance("RSA"); 469 | PrivateKey unencryptedPrivateKey = kf.generatePrivate(kspec); 470 | 471 | // System.out.println(privateKeyPEM); 472 | 473 | // PrivateKey privateKey = PrivateKeyFactory.createKey(new 474 | // ByteArrayInputStream(java.util.Base64.getDecoder().decode(privateKeyEncoded))); 475 | 476 | String pemEncodedCert = new String(java.util.Base64.getDecoder().decode(certEncoded)); 477 | Collection certs = pem2certs(pemEncodedCert); 478 | 479 | ks.setKeyEntry(alias, unencryptedPrivateKey, ksPass.toCharArray(), 480 | new Certificate[] { certs.iterator().next() }); 481 | 482 | } 483 | 484 | public static void importKeyPairAndCertPem(KeyStore ks, String ksPass, String alias, String privateKeyPEM, 485 | String pemEncodedCert) throws IOException, CertificateException, KeyStoreException, 486 | NoSuchAlgorithmException, InvalidKeySpecException { 487 | 488 | privateKeyPEM = privateKeyPEM.replace("-----BEGIN RSA PRIVATE KEY-----", "") 489 | .replace("-----END RSA PRIVATE KEY-----", "");// .trim();//.replaceAll("[\n,\r]", "").trim(); 490 | byte[] pkBytes = org.bouncycastle.util.encoders.Base64.decode(privateKeyPEM); 491 | PKCS8EncodedKeySpec kspec = new PKCS8EncodedKeySpec(pkBytes); 492 | KeyFactory kf = KeyFactory.getInstance("RSA"); 493 | PrivateKey unencryptedPrivateKey = kf.generatePrivate(kspec); 494 | 495 | // System.out.println(privateKeyPEM); 496 | 497 | // PrivateKey privateKey = PrivateKeyFactory.createKey(new 498 | // ByteArrayInputStream(java.util.Base64.getDecoder().decode(privateKeyEncoded))); 499 | 500 | Collection certs = pem2certs(pemEncodedCert); 501 | 502 | ks.setKeyEntry(alias, unencryptedPrivateKey, ksPass.toCharArray(), 503 | new Certificate[] { certs.iterator().next() }); 504 | 505 | } 506 | 507 | public static KeyStore decodeKeystore(String base64EncodedKS, String ksPassword) throws KeyStoreException { 508 | ByteArrayInputStream bais = new ByteArrayInputStream(java.util.Base64.getDecoder().decode(base64EncodedKS)); 509 | KeyStore newKS = KeyStore.getInstance("PKCS12"); 510 | try { 511 | newKS.load(bais, ksPassword.toCharArray()); 512 | return newKS; 513 | } catch (NoSuchAlgorithmException | CertificateException | IOException e) { 514 | return null; 515 | } 516 | } 517 | 518 | public static boolean keystoresEqual(KeyStore ks1, KeyStore ks2, String ksPassword) { 519 | 520 | try { 521 | HashSet checked = new HashSet(); 522 | 523 | Enumeration ks1Aliases = ks1.aliases(); 524 | 525 | while (ks1Aliases.hasMoreElements()) { 526 | String alias1 = ks1Aliases.nextElement(); 527 | X509Certificate cert1 = (X509Certificate) ks1.getCertificate(alias1); 528 | 529 | if (cert1 != null) { 530 | X509Certificate cert2 = (X509Certificate) ks2.getCertificate(alias1); 531 | if (cert2 == null) { 532 | return false; 533 | } else if (!Arrays.equals(cert1.getSignature(), cert2.getSignature())) { 534 | return false; 535 | } else { 536 | checked.add(alias1); 537 | } 538 | } else { 539 | SecretKey key1 = (SecretKey) ks1.getKey(alias1, ksPassword.toCharArray()); 540 | SecretKey key2 = (SecretKey) ks2.getKey(alias1, ksPassword.toCharArray()); 541 | if (key2 == null) { 542 | return false; 543 | } else if (!Arrays.equals(key1.getEncoded(), key2.getEncoded())) { 544 | return false; 545 | } else { 546 | checked.add(alias1); 547 | } 548 | } 549 | } 550 | 551 | Enumeration ks2Aliases = ks2.aliases(); 552 | while (ks2Aliases.hasMoreElements()) { 553 | if (!checked.contains(ks2Aliases.nextElement())) { 554 | // doesn't matter the content, its failed 555 | return false; 556 | } 557 | } 558 | 559 | return true; 560 | } catch (Exception e) { 561 | return false; 562 | } 563 | 564 | } 565 | 566 | public static Map exportCerts(KeyStore ks, String ksPwd) throws Exception { 567 | Map certs = new HashMap(); 568 | 569 | Enumeration aliases = ks.aliases(); 570 | 571 | while (aliases.hasMoreElements()) { 572 | String alias = aliases.nextElement(); 573 | X509Certificate cert = (X509Certificate) ks.getCertificate(alias); 574 | if (cert != null) { 575 | if (ks.getKey(alias, ksPwd.toCharArray()) == null) { 576 | certs.put(alias, CertUtils.exportCert(cert)); 577 | } 578 | } 579 | } 580 | 581 | return certs; 582 | } 583 | 584 | public static boolean isCertExpiring(X509Certificate cert, int daysOut) { 585 | DateTime expiresOn = new DateTime(cert.getNotAfter()); 586 | DateTime checkExpires = new DateTime().plusDays(daysOut); 587 | 588 | return checkExpires.isAfter(expiresOn); 589 | } 590 | 591 | public static KeyStore mergeCaCerts(KeyStore ks) throws KeyStoreException, NoSuchAlgorithmException, 592 | CertificateException, FileNotFoundException, IOException { 593 | KeyStore cacerts = KeyStore.getInstance(KeyStore.getDefaultType()); 594 | String cacertsPath = System.getProperty("javax.net.ssl.trustStore"); 595 | if (cacertsPath == null) { 596 | cacertsPath = System.getProperty("java.home") + "/lib/security/cacerts"; 597 | } 598 | 599 | cacerts.load(new FileInputStream(cacertsPath), null); 600 | 601 | Enumeration enumer = cacerts.aliases(); 602 | while (enumer.hasMoreElements()) { 603 | String alias = enumer.nextElement(); 604 | java.security.cert.Certificate cert = cacerts.getCertificate(alias); 605 | if (cert != null) { 606 | ks.setCertificateEntry(alias, cert); 607 | } 608 | } 609 | 610 | enumer = ks.aliases(); 611 | while (enumer.hasMoreElements()) { 612 | String alias = enumer.nextElement(); 613 | java.security.cert.Certificate cert = ks.getCertificate(alias); 614 | if (cert != null) { 615 | cacerts.setCertificateEntry(alias, cert); 616 | } 617 | } 618 | 619 | return cacerts; 620 | } 621 | } -------------------------------------------------------------------------------- /src/main/java/com/tremolosecurity/kubernetes/artifacts/util/K8sUtils.java: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Tremolo Security, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package com.tremolosecurity.kubernetes.artifacts.util; 16 | 17 | import java.io.BufferedReader; 18 | import java.io.ByteArrayInputStream; 19 | import java.io.File; 20 | import java.io.FileInputStream; 21 | import java.io.FileNotFoundException; 22 | import java.io.IOException; 23 | import java.io.InputStreamReader; 24 | import java.io.PrintStream; 25 | import java.io.UnsupportedEncodingException; 26 | import java.nio.charset.StandardCharsets; 27 | import java.nio.file.Files; 28 | import java.nio.file.Paths; 29 | import java.security.KeyManagementException; 30 | import java.security.KeyStore; 31 | import java.security.KeyStoreException; 32 | import java.security.MessageDigest; 33 | import java.security.NoSuchAlgorithmException; 34 | import java.security.UnrecoverableKeyException; 35 | import java.security.cert.CertificateException; 36 | import java.security.cert.X509Certificate; 37 | import java.time.format.DateTimeFormatter; 38 | import java.util.ArrayList; 39 | import java.util.Base64; 40 | import java.util.Enumeration; 41 | import java.util.HashMap; 42 | import java.util.HashSet; 43 | import java.util.List; 44 | import java.util.Map; 45 | import java.util.UUID; 46 | 47 | import javax.imageio.stream.FileImageInputStream; 48 | import javax.net.ssl.KeyManagerFactory; 49 | import javax.net.ssl.SSLContext; 50 | import javax.script.Invocable; 51 | import javax.script.ScriptContext; 52 | import javax.script.ScriptEngine; 53 | 54 | import com.fasterxml.jackson.core.JsonProcessingException; 55 | import com.fasterxml.jackson.databind.JsonNode; 56 | import com.fasterxml.jackson.databind.ObjectMapper; 57 | import com.fasterxml.jackson.dataformat.yaml.YAMLMapper; 58 | import com.tremolosecurity.kubernetes.artifacts.obj.HttpCon; 59 | import com.tremolosecurity.kubernetes.artifacts.run.Controller; 60 | 61 | import org.apache.http.Header; 62 | import org.apache.http.HttpResponse; 63 | import org.apache.http.client.ClientProtocolException; 64 | import org.apache.http.client.config.CookieSpecs; 65 | import org.apache.http.client.config.RequestConfig; 66 | import org.apache.http.client.entity.EntityBuilder; 67 | import org.apache.http.client.methods.HttpDelete; 68 | import org.apache.http.client.methods.HttpGet; 69 | import org.apache.http.client.methods.HttpPatch; 70 | import org.apache.http.client.methods.HttpPost; 71 | import org.apache.http.client.methods.HttpPut; 72 | import org.apache.http.config.Registry; 73 | import org.apache.http.config.RegistryBuilder; 74 | import org.apache.http.conn.socket.ConnectionSocketFactory; 75 | import org.apache.http.conn.socket.PlainConnectionSocketFactory; 76 | import org.apache.http.conn.ssl.SSLConnectionSocketFactory; 77 | import org.apache.http.conn.ssl.SSLContexts; 78 | import org.apache.http.entity.ContentType; 79 | import org.apache.http.entity.StringEntity; 80 | import org.apache.http.impl.client.CloseableHttpClient; 81 | import org.apache.http.impl.client.HttpClients; 82 | import org.apache.http.impl.conn.BasicHttpClientConnectionManager; 83 | import org.apache.http.message.BasicHeader; 84 | import org.apache.http.util.EntityUtils; 85 | import org.joda.time.DateTime; 86 | import org.joda.time.format.DateTimeFormat; 87 | import org.json.simple.JSONArray; 88 | import org.json.simple.JSONObject; 89 | import org.json.simple.parser.JSONParser; 90 | 91 | import io.kubernetes.client.openapi.ApiClient; 92 | import io.kubernetes.client.openapi.Configuration; 93 | import io.kubernetes.client.util.ClientBuilder; 94 | import io.kubernetes.client.util.KubeConfig; 95 | 96 | /** 97 | * K8sUtils 98 | * 99 | * Utilities for interacting with the Kubernetes API server 100 | */ 101 | public class K8sUtils { 102 | String token; 103 | private KeyStore ks; 104 | private String ksPassword; 105 | private KeyManagerFactory kmf; 106 | private Registry httpClientRegistry; 107 | private RequestConfig globalHttpClientConfig; 108 | String url; 109 | String pathToCaCert; 110 | String caCert; 111 | ScriptEngine engine; 112 | 113 | private Map additionalStatuses; 114 | 115 | private boolean openShift; 116 | 117 | private Map extraCerts; 118 | 119 | static HashSet processedVersions = new HashSet(); 120 | 121 | boolean fromKc; 122 | String pathToToken; 123 | 124 | /** 125 | * Initialization 126 | * 127 | * @param pathToToken 128 | * @param pathToCA 129 | * @param pathToMoreCerts 130 | * @param apiServerURL 131 | * @throws Exception 132 | */ 133 | public K8sUtils(String pathToToken, String pathToCA, String pathToMoreCerts, String apiServerURL) throws Exception { 134 | // get the token for talking to k8s 135 | this.pathToToken = pathToToken; 136 | this.token = null; 137 | this.fromKc = false; 138 | 139 | this.pathToCaCert = pathToCA; 140 | 141 | this.url = apiServerURL; 142 | 143 | this.extraCerts = new HashMap(); 144 | 145 | this.ksPassword = UUID.randomUUID().toString(); 146 | this.ks = KeyStore.getInstance("PKCS12"); 147 | this.ks.load(null, this.ksPassword.toCharArray()); 148 | 149 | if (System.getenv().get("KUBECONFIG") != null) { 150 | this.fromKc = true; 151 | String pathToKubeConfig = System.getenv("KUBECONFIG"); 152 | System.out.println("******* OVERRIDING WITH KUBECONFIG FROM '" + pathToKubeConfig + "' ******************"); 153 | 154 | 155 | KubeConfig kc = KubeConfig.loadKubeConfig(new InputStreamReader(new FileInputStream(pathToKubeConfig))); 156 | String context = kc.getCurrentContext(); 157 | this.token = kc.getAccessToken(); 158 | 159 | this.url = kc.getServer(); 160 | 161 | if (token == null) { 162 | if (kc.getClientKeyData() != null) { 163 | String pemKey = new String(Base64.getDecoder().decode(kc.getClientKeyData())); 164 | String pemCert = new String(Base64.getDecoder().decode(kc.getClientCertificateData())); 165 | 166 | CertUtils.importKeyPairAndCertPem(this.ks, this.ksPassword, "k8sclient", pemKey, pemCert); 167 | } 168 | 169 | 170 | } 171 | 172 | if (kc.getCertificateAuthorityData() != null) { 173 | CertUtils.importCertificate(ks, ksPassword, "k8s-master", new String(Base64.getDecoder().decode(kc.getCertificateAuthorityData()))); 174 | } 175 | } else { 176 | caCert = new String(Files.readAllBytes(Paths.get(pathToCA)), StandardCharsets.UTF_8); 177 | CertUtils.importCertificate(ks, ksPassword, "k8s-master", caCert); 178 | } 179 | 180 | 181 | 182 | 183 | 184 | File moreCerts = new File(pathToMoreCerts); 185 | if (moreCerts.exists() && moreCerts.isDirectory()) { 186 | for (File certFile : moreCerts.listFiles()) { 187 | System.out.println("Processing - '" + certFile.getAbsolutePath() + "'"); 188 | if (certFile.isDirectory() || !certFile.getAbsolutePath().toLowerCase().endsWith(".pem")) { 189 | System.out.println("not a pem, skipping"); 190 | continue; 191 | } 192 | String certPem = new String(Files.readAllBytes(Paths.get(certFile.getAbsolutePath())), 193 | StandardCharsets.UTF_8); 194 | String alias = certFile.getName().substring(0, certFile.getName().indexOf('.')); 195 | this.extraCerts.put(alias, certPem); 196 | CertUtils.importCertificate(ks, ksPassword, alias, certPem); 197 | } 198 | } 199 | 200 | KeyStore cacerts = KeyStore.getInstance(KeyStore.getDefaultType()); 201 | String cacertsPath = System.getProperty("javax.net.ssl.trustStore"); 202 | if (cacertsPath == null) { 203 | cacertsPath = System.getProperty("java.home") + "/lib/security/cacerts"; 204 | } 205 | 206 | cacerts.load(new FileInputStream(cacertsPath), null); 207 | 208 | Enumeration enumer = cacerts.aliases(); 209 | while (enumer.hasMoreElements()) { 210 | String alias = enumer.nextElement(); 211 | java.security.cert.Certificate cert = cacerts.getCertificate(alias); 212 | ks.setCertificateEntry(alias, cert); 213 | } 214 | 215 | this.kmf = KeyManagerFactory.getInstance("SunX509"); 216 | kmf.init(this.ks, this.ksPassword.toCharArray()); 217 | 218 | SSLContext sslctx = SSLContexts.custom().loadTrustMaterial(this.ks) 219 | .loadKeyMaterial(this.ks, this.ksPassword.toCharArray()).build(); 220 | SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(sslctx, 221 | SSLConnectionSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER); 222 | 223 | PlainConnectionSocketFactory sf = PlainConnectionSocketFactory.getSocketFactory(); 224 | this.httpClientRegistry = RegistryBuilder.create().register("http", sf) 225 | .register("https", sslsf).build(); 226 | 227 | this.globalHttpClientConfig = RequestConfig.custom().setCookieSpec(CookieSpecs.IGNORE_COOKIES) 228 | .setRedirectsEnabled(false).setAuthenticationEnabled(false).build(); 229 | 230 | 231 | 232 | this.openShift = false; 233 | 234 | Map res = this.callWS("/apis"); 235 | String json = (String) res.get("data"); 236 | 237 | 238 | JSONParser parser = new JSONParser(); 239 | JSONObject root = (JSONObject) parser.parse(json); 240 | JSONArray groups = (JSONArray) root.get("groups"); 241 | 242 | for (Object obj : groups) { 243 | JSONObject group = (JSONObject) obj; 244 | String name = (String) group.get("name"); 245 | if (name.toLowerCase().contains("openshift")) { 246 | this.openShift = true; 247 | break; 248 | } 249 | } 250 | 251 | additionalStatuses = new HashMap(); 252 | } 253 | 254 | /** 255 | * Generate an HTTP client pre-configured with the container's service-account 256 | * token and trust of the api server's certificate 257 | * 258 | * @return 259 | * @throws Exception 260 | */ 261 | public HttpCon createClient() throws Exception { 262 | ArrayList
defheaders = new ArrayList
(); 263 | defheaders.add(new BasicHeader("X-Csrf-Token", "1")); 264 | 265 | BasicHttpClientConnectionManager bhcm = new BasicHttpClientConnectionManager(this.httpClientRegistry); 266 | 267 | RequestConfig rc = RequestConfig.custom().setCookieSpec(CookieSpecs.STANDARD).setRedirectsEnabled(false) 268 | .build(); 269 | 270 | CloseableHttpClient http = HttpClients.custom().setConnectionManager(bhcm).setDefaultHeaders(defheaders) 271 | .setDefaultRequestConfig(rc).build(); 272 | 273 | HttpCon con = new HttpCon(); 274 | con.setBcm(bhcm); 275 | con.setHttp(http); 276 | 277 | return con; 278 | 279 | } 280 | 281 | /** 282 | * Call a kubernetes web service via GET 283 | * 284 | * @param uri 285 | * @return 286 | * @throws Exception 287 | */ 288 | public Map callWS(String uri) throws Exception { 289 | return callWS(uri, null, 10); 290 | } 291 | 292 | /** 293 | * Watch a Kubernetes object based on its URI 294 | * 295 | * @param uri 296 | * @throws Exception 297 | */ 298 | public void watchURI(String uri, String functionName) throws Exception { 299 | 300 | 301 | 302 | 303 | K8sUtils localK8s = new K8sUtils(Controller.tokenPath, Controller.rootCaPath, Controller.configMaps, 304 | Controller.kubernetesURL); 305 | ScriptEngine localEngine = Controller.initializeJS(Controller.jsPath, Controller.namespace, localK8s); 306 | localK8s.setEngine(localEngine); 307 | StringBuffer b = new StringBuffer(); 308 | 309 | System.out.println(uri); 310 | 311 | System.out.println("Is OpenShift : " + localK8s.isOpenShift()); 312 | 313 | b.append(this.getK8sUrl()).append(uri); 314 | HttpGet get = new HttpGet(b.toString()); 315 | String ltoken = this.getAuthorizationToken(); 316 | if (ltoken != null) { 317 | 318 | b.setLength(0); 319 | b.append("Bearer ").append(ltoken); 320 | get.addHeader(new BasicHeader("Authorization", "Bearer " + ltoken)); 321 | } 322 | 323 | HttpCon con = this.createClient(); 324 | 325 | try { 326 | HttpResponse resp = con.getHttp().execute(get); 327 | 328 | BufferedReader in = new BufferedReader(new InputStreamReader(resp.getEntity().getContent())); 329 | 330 | String line = null; 331 | while ((line = in.readLine()) != null) { 332 | 333 | JSONParser parser = new JSONParser(); 334 | JSONObject json = (JSONObject) parser.parse(line); 335 | 336 | 337 | JSONObject cr = (JSONObject) json.get("object"); 338 | JSONObject chkObj = new JSONObject(); 339 | chkObj.put("apiVersion", cr.get("apiVersion")); 340 | chkObj.put("kind", cr.get("kind")); 341 | chkObj.put("spec", cr.get("spec")); 342 | 343 | JSONObject metadata = (JSONObject) parser.parse(((JSONObject)cr.get("metadata")).toJSONString()); 344 | 345 | String resourceVersion = (String) metadata.get("resourceVersion"); 346 | 347 | if (resourceVersion == null) { 348 | System.out.println("unexpected json - " + line); 349 | throw new Exception("No resourceVersion, restartinig watch"); 350 | } 351 | 352 | System.out.println("Resource Version - " + resourceVersion + " - " + processedVersions.contains(resourceVersion)); 353 | 354 | if (processedVersions.contains(resourceVersion)) { 355 | System.out.println("Resource - " + resourceVersion + " - already processed, skipping"); 356 | continue; 357 | } else { 358 | processedVersions.add(resourceVersion); 359 | } 360 | 361 | metadata.remove("creationTimestamp"); 362 | metadata.remove("generation"); 363 | metadata.remove("resourceVersion"); 364 | metadata.remove("managedFields"); 365 | 366 | 367 | chkObj.put("metadata", metadata); 368 | 369 | String jsonForChecksum = chkObj.toJSONString(); 370 | MessageDigest digest = java.security.MessageDigest.getInstance("SHA-256"); 371 | digest.update(jsonForChecksum.getBytes("UTF-8"),0,jsonForChecksum.getBytes("UTF-8").length); 372 | byte[] digestBytes = digest.digest(); 373 | String digestBase64 = java.util.Base64.getEncoder().encodeToString(digestBytes); 374 | 375 | if (json.get("type").equals("MODIFIED")) { 376 | if (json.get("object") != null && ((JSONObject) json.get("object")).get("status") != null && ((JSONObject) ((JSONObject) json.get("object")).get("status")).get("digest") != null) { 377 | String existingDigest = (String) ((JSONObject) ((JSONObject) json.get("object")).get("status")).get("digest"); 378 | if (existingDigest.equals(digestBase64)) { 379 | continue; 380 | } 381 | } 382 | } 383 | 384 | Invocable invocable = (Invocable) localEngine; 385 | 386 | boolean error = false; 387 | 388 | 389 | String result = null; 390 | 391 | try { 392 | System.out.println("Starting javascript"); 393 | result = (String) invocable.invokeFunction(functionName, line); 394 | System.out.println("javascript completed"); 395 | } catch (Throwable t) { 396 | System.err.println("Error on watch - " + uri); 397 | t.printStackTrace(System.err); 398 | error = true; 399 | } 400 | 401 | if (error) { 402 | if (result != null) { 403 | result = "error-" + result; 404 | } else { 405 | result = "error"; 406 | } 407 | } 408 | 409 | System.out.println("Creating status for '" + json.get("type") + "'"); 410 | if (json.get("type").equals("MODIFIED") || json.get("type").equals("ADDED")) { 411 | System.out.println("Creating status"); 412 | JSONObject patch = this.generateJsonStatus(result, digestBase64,localK8s.getAdditionalStatuses()); 413 | cr.put("status",patch); 414 | 415 | String selfUri = uri; 416 | if (selfUri.contains("?")) { 417 | selfUri = selfUri.substring(0,selfUri.indexOf('?')); 418 | } 419 | 420 | System.out.println("Patching : '" + selfUri + "'"); 421 | System.out.println("New status : " + cr.toJSONString()); 422 | 423 | //String selfLink = (String) ((JSONObject) ((JSONObject) json.get("object")).get("metadata")).get("selfLink"); 424 | 425 | this.putWS(selfUri + "/status", cr.toJSONString()); 426 | 427 | 428 | } 429 | 430 | 431 | 432 | 433 | } 434 | 435 | } finally { 436 | if (con != null) { 437 | con.getBcm().shutdown(); 438 | } 439 | } 440 | 441 | } 442 | 443 | private JSONObject generateJsonStatus(String errorMessage,String digest, Map additionalStatuses) { 444 | JSONObject patch = new JSONObject(); 445 | 446 | patch.put("conditions", new JSONObject()); 447 | ((JSONObject) patch.get("conditions")).put("lastTransitionTime", DateTimeFormat.forPattern("yyyy-MM-dd hh:mm:ssz").print(new DateTime())); 448 | patch.put("digest", digest); 449 | 450 | if (errorMessage == null) { 451 | ((JSONObject) patch.get("conditions")).put("status", "True"); 452 | ((JSONObject) patch.get("conditions")).put("type", "Completed"); 453 | } else { 454 | ((JSONObject) patch.get("conditions")).put("status", "True"); 455 | ((JSONObject) patch.get("conditions")).put("type", "Failed"); 456 | ((JSONObject) patch.get("conditions")).put("status", "True"); 457 | ((JSONObject) patch.get("conditions")).put("reason", errorMessage); 458 | } 459 | 460 | for (String extraStatus : additionalStatuses.keySet()) { 461 | this.addToJson(patch, extraStatus, additionalStatuses.get(extraStatus)); 462 | } 463 | 464 | return patch; 465 | } 466 | 467 | private void addToJson(JSONObject root,String name,Object value) { 468 | if (value instanceof String) { 469 | root.put(name,value); 470 | } else if (value instanceof Map) { 471 | JSONObject newRoot = new JSONObject(); 472 | root.put(name, newRoot); 473 | Map rootSet = (Map) value; 474 | for (String keyName : rootSet.keySet()) { 475 | addToJson(newRoot, keyName, rootSet.get(keyName)); 476 | } 477 | } else { 478 | System.out.println("Unknown type" + value); 479 | } 480 | } 481 | 482 | /** 483 | * GET an API via its URI, with a test function for success and a number of 484 | * attempted retries 485 | * 486 | * @param uri 487 | * @param testFunction 488 | * @param count 489 | * @return 490 | * @throws Exception 491 | */ 492 | public Map callWS(String uri, String testFunction, int count) throws Exception { 493 | 494 | StringBuffer b = new StringBuffer(); 495 | 496 | b.append(this.getK8sUrl()).append(uri); 497 | HttpGet get = new HttpGet(b.toString()); 498 | String ltoken = this.getAuthorizationToken(); 499 | if (ltoken != null) { 500 | b.setLength(0); 501 | b.append("Bearer ").append(ltoken); 502 | get.addHeader(new BasicHeader("Authorization", "Bearer " + ltoken)); 503 | } 504 | 505 | HttpCon con = this.createClient(); 506 | try { 507 | HttpResponse resp = con.getHttp().execute(get); 508 | String json = EntityUtils.toString(resp.getEntity()); 509 | Map ret = new HashMap(); 510 | ret.put("code", resp.getStatusLine().getStatusCode()); 511 | ret.put("data", json); 512 | 513 | if (count >= 0 514 | && (resp.getStatusLine().getStatusCode() < 200 || resp.getStatusLine().getStatusCode() > 299)) { 515 | System.err.println("Problem calling '" + uri + "' - " + resp.getStatusLine().getStatusCode()); 516 | System.err.println(json); 517 | 518 | if (count > 0) { 519 | System.err.println("Sleeping, then trying again"); 520 | Thread.sleep(10000); 521 | System.err.println("trying again"); 522 | return callWS(uri, testFunction, --count); 523 | 524 | } 525 | } 526 | 527 | if (testFunction != null && ! testFunction.isEmpty()) { 528 | engine.getBindings(ScriptContext.ENGINE_SCOPE).put("check_ws_response", false); 529 | engine.getBindings(ScriptContext.ENGINE_SCOPE).put("ws_response_json", json); 530 | 531 | try { 532 | engine.eval(testFunction); 533 | } catch (Throwable t) { 534 | System.err.println("Unable to verify '" + uri + "' / " + json); 535 | t.printStackTrace(); 536 | if (count > 0) { 537 | System.err.println("Sleeping, then trying again"); 538 | Thread.sleep(10000); 539 | System.err.println("trying again"); 540 | return callWS(uri, testFunction, --count); 541 | 542 | } 543 | } 544 | 545 | if (!((Boolean) engine.getBindings(ScriptContext.ENGINE_SCOPE).get("check_ws_response"))) { 546 | System.err.println("Verification for '" + uri + "' failed / " + json); 547 | if (count > 0) { 548 | System.err.println("Sleeping, then trying again"); 549 | Thread.sleep(10000); 550 | System.err.println("trying again"); 551 | return callWS(uri, testFunction, --count); 552 | 553 | } 554 | } 555 | } 556 | 557 | return ret; 558 | } finally { 559 | if (con != null) { 560 | con.getBcm().shutdown(); 561 | } 562 | } 563 | } 564 | 565 | public Map deleteWS(String uri) throws Exception { 566 | return deleteWS(uri,true); 567 | } 568 | 569 | /** 570 | * DELETE a Kubernetes object 571 | * 572 | * @param uri 573 | * @param ignoreNotFound 574 | * @return 575 | * @throws Exception 576 | */ 577 | public Map deleteWS(String uri,boolean ignoreNotFound) throws Exception { 578 | 579 | StringBuffer b = new StringBuffer(); 580 | 581 | b.append(this.getK8sUrl()).append(uri); 582 | HttpDelete delete = new HttpDelete(b.toString()); 583 | String ltoken = this.getAuthorizationToken(); 584 | if (ltoken != null) { 585 | b.setLength(0); 586 | b.append("Bearer ").append(ltoken); 587 | delete.addHeader(new BasicHeader("Authorization", "Bearer " + ltoken)); 588 | } 589 | 590 | HttpCon con = this.createClient(); 591 | try { 592 | HttpResponse resp = con.getHttp().execute(delete); 593 | String json = EntityUtils.toString(resp.getEntity()); 594 | Map ret = new HashMap(); 595 | ret.put("code", resp.getStatusLine().getStatusCode()); 596 | ret.put("data", json); 597 | 598 | if (resp.getStatusLine().getStatusCode() < 200 || resp.getStatusLine().getStatusCode() > 299) { 599 | if (! (resp.getStatusLine().getStatusCode() == 404 && ignoreNotFound)) { 600 | System.err.println("Problem calling '" + uri + "' - " + resp.getStatusLine().getStatusCode()); 601 | System.err.println(json); 602 | } 603 | } 604 | 605 | return ret; 606 | } finally { 607 | if (con != null) { 608 | con.getBcm().shutdown(); 609 | } 610 | } 611 | } 612 | 613 | /** 614 | * POST to an API URI 615 | * 616 | * @param uri 617 | * @param json 618 | * @return 619 | * @throws Exception 620 | */ 621 | public Map postWS(String uri, String json) throws Exception { 622 | StringBuffer b = new StringBuffer(); 623 | 624 | b.append(this.getK8sUrl()).append(uri); 625 | HttpPost post = new HttpPost(b.toString()); 626 | String ltoken = this.getAuthorizationToken(); 627 | if (ltoken != null) { 628 | b.setLength(0); 629 | b.append("Bearer ").append(ltoken); 630 | post.addHeader(new BasicHeader("Authorization", "Bearer " + ltoken)); 631 | } 632 | 633 | StringEntity str = new StringEntity(json, ContentType.APPLICATION_JSON); 634 | post.setEntity(str); 635 | 636 | HttpCon con = this.createClient(); 637 | try { 638 | HttpResponse resp = con.getHttp().execute(post); 639 | String jsonResponse = EntityUtils.toString(resp.getEntity()); 640 | Map ret = new HashMap(); 641 | ret.put("code", resp.getStatusLine().getStatusCode()); 642 | ret.put("data", jsonResponse); 643 | 644 | if (resp.getStatusLine().getStatusCode() < 200 || resp.getStatusLine().getStatusCode() > 299) { 645 | System.err.println("Problem calling '" + uri + "' - " + resp.getStatusLine().getStatusCode()); 646 | System.err.println(json); 647 | } 648 | 649 | return ret; 650 | } finally { 651 | if (con != null) { 652 | con.getBcm().shutdown(); 653 | } 654 | } 655 | } 656 | 657 | 658 | /** 659 | * PUT to a URI 660 | * 661 | * @param uri 662 | * @param json 663 | * @return 664 | * @throws Exception 665 | */ 666 | public Map patchWS(String uri, String json) throws Exception { 667 | StringBuffer b = new StringBuffer(); 668 | 669 | b.append(this.getK8sUrl()).append(uri); 670 | HttpPatch patch = new HttpPatch(b.toString()); 671 | String ltoken = this.getAuthorizationToken(); 672 | if (ltoken != null) { 673 | b.setLength(0); 674 | b.append("Bearer ").append(ltoken); 675 | patch.addHeader(new BasicHeader("Authorization", "Bearer " + ltoken)); 676 | } 677 | patch.setEntity(EntityBuilder.create().setContentType(ContentType.create("application/merge-patch+json")).setText(json).build()); 678 | 679 | 680 | 681 | HttpCon con = this.createClient(); 682 | try { 683 | HttpResponse resp = con.getHttp().execute(patch); 684 | String jsonResponse = EntityUtils.toString(resp.getEntity()); 685 | Map ret = new HashMap(); 686 | ret.put("code", resp.getStatusLine().getStatusCode()); 687 | ret.put("data", jsonResponse); 688 | 689 | if (resp.getStatusLine().getStatusCode() < 200 || resp.getStatusLine().getStatusCode() > 299) { 690 | System.err.println("Problem calling '" + uri + "' - " + resp.getStatusLine().getStatusCode()); 691 | System.err.println(json); 692 | } 693 | 694 | return ret; 695 | } finally { 696 | if (con != null) { 697 | con.getBcm().shutdown(); 698 | } 699 | } 700 | } 701 | 702 | /** 703 | * PUT to a URI 704 | * 705 | * @param uri 706 | * @param json 707 | * @return 708 | * @throws Exception 709 | */ 710 | public Map putWS(String uri, String json) throws Exception { 711 | StringBuffer b = new StringBuffer(); 712 | 713 | b.append(this.getK8sUrl()).append(uri); 714 | HttpPut post = new HttpPut(b.toString()); 715 | String ltoken = this.getAuthorizationToken(); 716 | if (ltoken != null) { 717 | b.setLength(0); 718 | b.append("Bearer ").append(ltoken); 719 | post.addHeader(new BasicHeader("Authorization", "Bearer " + ltoken)); 720 | } 721 | StringEntity str = new StringEntity(json, ContentType.APPLICATION_JSON); 722 | post.setEntity(str); 723 | 724 | HttpCon con = this.createClient(); 725 | try { 726 | HttpResponse resp = con.getHttp().execute(post); 727 | String jsonResponse = EntityUtils.toString(resp.getEntity()); 728 | Map ret = new HashMap(); 729 | ret.put("code", resp.getStatusLine().getStatusCode()); 730 | ret.put("data", jsonResponse); 731 | 732 | if (resp.getStatusLine().getStatusCode() < 200 || resp.getStatusLine().getStatusCode() > 299) { 733 | System.err.println("Problem calling '" + uri + "' - " + resp.getStatusLine().getStatusCode()); 734 | System.err.println(jsonResponse); 735 | } 736 | 737 | return ret; 738 | } finally { 739 | if (con != null) { 740 | con.getBcm().shutdown(); 741 | } 742 | } 743 | } 744 | 745 | /** 746 | * Returns a certificate from the internal keystore 747 | * 748 | * @param name 749 | * @return 750 | * @throws KeyStoreException 751 | */ 752 | public X509Certificate getCertificate(String name) throws KeyStoreException { 753 | return (X509Certificate) this.ks.getCertificate(name); 754 | } 755 | 756 | /** 757 | * Base64 encode a Map of name/value pairs 758 | * 759 | * @param data 760 | * @return 761 | * @throws UnsupportedEncodingException 762 | */ 763 | public String encodeMap(Map data) throws UnsupportedEncodingException { 764 | String vals = ""; 765 | for (Object k : data.keySet()) { 766 | vals += k + "=" + data.get(k) + "\n"; 767 | } 768 | vals = vals.substring(0, vals.length() - 1); 769 | return Base64.getEncoder().encodeToString(vals.getBytes("UTF-8")); 770 | } 771 | 772 | /** 773 | * Simple template processor replacing name/value pairs from the map to anything 774 | * enclused in #[] so #[MY_VALUE] would be replaced with the value from the map 775 | * associated with the key MY_VALUE 776 | * 777 | * @param template 778 | * @param vars 779 | * @return 780 | */ 781 | public String processTemplate(String template, Map vars) { 782 | StringBuffer newConfig = new StringBuffer(); 783 | newConfig.setLength(0); 784 | 785 | int begin, end; 786 | 787 | begin = 0; 788 | end = 0; 789 | 790 | String finalCfg = null; 791 | 792 | begin = template.indexOf("#["); 793 | while (begin > 0) { 794 | if (end == 0) { 795 | newConfig.append(template.substring(0, begin)); 796 | } else { 797 | newConfig.append(template.substring(end, begin)); 798 | } 799 | 800 | end = template.indexOf(']', begin + 2); 801 | 802 | String envVarName = template.substring(begin + 2, end); 803 | String value = (String) vars.get(envVarName); 804 | 805 | if (value == null) { 806 | value = ""; 807 | } 808 | 809 | newConfig.append(value); 810 | 811 | begin = template.indexOf("#[", end + 1); 812 | end++; 813 | } 814 | 815 | if (end != 0) { 816 | newConfig.append(template.substring(end)); 817 | } 818 | 819 | return newConfig.toString(); 820 | } 821 | 822 | /** 823 | * Run kubectl create with the data passed in using the service account of the 824 | * container 825 | * 826 | * @param data 827 | * @throws IOException 828 | * @throws InterruptedException 829 | */ 830 | public void kubectlCreate(String data) throws IOException, InterruptedException { 831 | String ltoken = this.getAuthorizationToken(); 832 | Process p = Runtime.getRuntime().exec(new String[] { "kubectl", "--token=" + ltoken, "--server=" + this.getK8sUrl(), 833 | "--certificate-authority=" + this.pathToCaCert, "create", "-f", "-" }); 834 | 835 | new Thread() { 836 | public void run() { 837 | BufferedReader in = new BufferedReader(new InputStreamReader(p.getInputStream())); 838 | String line; 839 | try { 840 | while ((line = in.readLine()) != null) { 841 | System.out.println(line); 842 | } 843 | } catch (IOException e) { 844 | e.printStackTrace(); 845 | } 846 | } 847 | }.start(); 848 | 849 | new Thread() { 850 | public void run() { 851 | BufferedReader in = new BufferedReader(new InputStreamReader(p.getErrorStream())); 852 | String line; 853 | try { 854 | while ((line = in.readLine()) != null) { 855 | System.err.println(line); 856 | } 857 | } catch (IOException e) { 858 | e.printStackTrace(); 859 | } 860 | } 861 | }.start(); 862 | 863 | PrintStream out = new PrintStream(p.getOutputStream()); 864 | BufferedReader in = new BufferedReader(new InputStreamReader(new ByteArrayInputStream(data.getBytes("UTF-8")))); 865 | String line; 866 | while ((line = in.readLine()) != null) { 867 | out.println(line); 868 | } 869 | 870 | out.close(); 871 | System.out.println("waiting for completion"); 872 | 873 | p.waitFor(); 874 | 875 | } 876 | 877 | /** 878 | * @param engine the engine to set 879 | */ 880 | public void setEngine(ScriptEngine engine) { 881 | this.engine = engine; 882 | } 883 | 884 | /** 885 | * @return the engine 886 | */ 887 | public ScriptEngine getEngine() { 888 | return engine; 889 | } 890 | 891 | public String json2yaml(String json) throws IOException { 892 | JsonNode jsonNodeTree = new ObjectMapper().readTree(json); 893 | String jsonAsYaml = new YAMLMapper().writeValueAsString(jsonNodeTree); 894 | return jsonAsYaml; 895 | 896 | } 897 | 898 | public String getCaCert() { 899 | return this.caCert; 900 | } 901 | 902 | public boolean isOpenShift() { 903 | return this.openShift; 904 | } 905 | 906 | 907 | /** 908 | * @return the additionalStatuses 909 | */ 910 | public Map getAdditionalStatuses() { 911 | return additionalStatuses; 912 | } 913 | 914 | public KeyStore getKs() { 915 | return this.ks; 916 | } 917 | 918 | public String getKsPassword() { 919 | return this.ksPassword; 920 | } 921 | 922 | public Map getExtraCerts() { 923 | return this.extraCerts; 924 | } 925 | 926 | public String getK8sUrl() { 927 | if (this.url.equalsIgnoreCase("https://kubernetes.default.svc.cluster.local")) { 928 | //we want to use the default URL. Instead of using it, we'll build it from 929 | //the environment variables 930 | return new StringBuilder().append("https://").append(System.getenv("KUBERNETES_SERVICE_HOST")).append(":").append(System.getenv("KUBERNETES_SERVICE_PORT")).toString(); 931 | } else { 932 | return this.url; 933 | } 934 | } 935 | 936 | public String getAuthorizationToken() throws IOException { 937 | if (this.fromKc) { 938 | return this.token; 939 | } else { 940 | return new String(Files.readAllBytes(Paths.get(pathToToken)), StandardCharsets.UTF_8); 941 | } 942 | } 943 | } --------------------------------------------------------------------------------