Skip to content

Commit

Permalink
Merge pull request #12 from StatCan/configurable-instances
Browse files Browse the repository at this point in the history
Configurable instances
  • Loading branch information
Zachary Seguin authored Sep 30, 2021
2 parents 7b8709e + 16351d2 commit 2bb5e5e
Show file tree
Hide file tree
Showing 4 changed files with 132 additions and 47 deletions.
19 changes: 18 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,19 @@
# minio-credential-injector
This mutating webhook adds minio credential annotations to notebook pods.

This mutating webhook adds minio credential annotations to notebook pods and argo workflows (used by Kubeflow Pipelines).

To configure use with different instances, put a `instances.json` file in the working directory. For example

```json
{"name": "minio_standard", "classification": "unclassified", "serviceUrl": "http://minio.minio-standard-system:443"}
{"name": "minio_premium", "classification": "unclassified", "serviceUrl": "http://minio.minio-premium-system:443"}
```

Try it with

```sh
./minio-credential-injector &
curl --insecure -X POST -H "Content-Type: application/json" \
-d @samples/pod.json https://0.0.0.0:8443/mutate |
jq -r '.response.patch | @base64d' | jq
```
54 changes: 49 additions & 5 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,31 @@ package main
import (
"encoding/json"
"fmt"
"io"
"io/ioutil"
"log"
"net/http"
"os"
"strings"
"time"

"k8s.io/api/admission/v1beta1"
)

// Based on https://medium.com/ovni/writing-a-very-basic-kubernetes-mutating-admission-webhook-398dbbcb63ec
type Instance struct {
Name string
Classification string
ServiceUrl string
}

var instances []Instance
var defaultInstances = `
{"name": "minio_standard", "classification": "unclassified", "serviceUrl": "http://minio.minio-standard-system:443"}
{"name": "minio_premium", "classification": "unclassified", "serviceUrl": "http://minio.minio-premium-system:443"}
{"name": "minio_protected_b", "classification": "protected-b", "serviceUrl": "http://minio.minio-protected-b-system:443"}
`

// Based on https://medium.com/ovni/writing-a-very-basic-kubernetes-mutating-admission-webhook-398dbbcb63ec
func handleRoot(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, world!")
}
Expand Down Expand Up @@ -40,10 +55,7 @@ func handleMutate(w http.ResponseWriter, r *http.Request) {
return
}

response, err := mutate(*admissionReview.Request, map[string][]string{
"unclassified": []string{"minio_standard", "minio_premium"},
"protected-b": []string{"minio_protected_b"},
})
response, err := mutate(*admissionReview.Request, instances)
if err != nil {
log.Println(err)
w.WriteHeader(http.StatusInternalServerError)
Expand All @@ -66,7 +78,39 @@ func handleMutate(w http.ResponseWriter, r *http.Request) {
w.Write(body)
}

// Sets the global instances variable
func configInstances() {
var config string
if _, err := os.Stat("instances.json"); os.IsNotExist(err) {
config = defaultInstances
} else {
config_bytes, err := ioutil.ReadFile("instances.json") // just pass the file name
if err != nil {
log.Fatal(err)
}
config = string(config_bytes)
}

dec := json.NewDecoder(strings.NewReader(config))
for {
var instance Instance
err := dec.Decode(&instance)
if err != nil {
if err == io.EOF {
break
}
log.Fatal(err)
}
fmt.Println(instance)
instances = append(instances, instance)
}
}

func main() {

// Configure the MinIO Instances
configInstances()

mux := http.NewServeMux()

mux.HandleFunc("/", handleRoot)
Expand Down
101 changes: 61 additions & 40 deletions mutate.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,39 +15,41 @@ func cleanName(name string) string {
return strings.ReplaceAll(name, "_", "-")
}

func mutate(request v1beta1.AdmissionRequest, instances map[string][]string) (v1beta1.AdmissionResponse, error) {
response := v1beta1.AdmissionResponse{}

// Default response
response.Allowed = true
response.UID = request.UID
func shouldInject(pod *v1.Pod) bool {

// Decode the pod object
var err error
pod := v1.Pod{}
if err := json.Unmarshal(request.Object.Raw, &pod); err != nil {
return response, fmt.Errorf("unable to decode Pod %w", err)
}

log.Printf("Check pod for notebook or workflow %s/%s", pod.Namespace, pod.Name)

shouldInject := false
// Inject Minio credentials into notebook pods (condition: has notebook-name label)
if _, ok := pod.ObjectMeta.Labels["notebook-name"]; ok {
log.Printf("Found notebook name for %s/%s", pod.Namespace, pod.Name)
shouldInject = true
log.Printf("Found notebook name for %s/%s; injecting", pod.Namespace, pod.Name)
return true
}

// Inject Minio credentials into argo workflow pods (condition: has workflows.argoproj.io/workflow label)
if _, ok := pod.ObjectMeta.Labels["workflows.argoproj.io/workflow"]; ok {
log.Printf("Found argo workflow name for %s/%s", pod.Namespace, pod.Name)
shouldInject = true
log.Printf("Found argo workflow name for %s/%s; injecting", pod.Namespace, pod.Name)
return true
}

// Inject Minio credentials into pod requesting credentials (condition: has add-default-minio-creds annotation)
if _, ok := pod.ObjectMeta.Annotations["data.statcan.gc.ca/inject-minio-creds"]; ok {
log.Printf("Found minio credential annotation on %s/%s", pod.Namespace, pod.Name)
shouldInject = true
log.Printf("Found minio credential annotation on %s/%s; injecting", pod.Namespace, pod.Name)
return true
}

return false
}

func mutate(request v1beta1.AdmissionRequest, instances []Instance) (v1beta1.AdmissionResponse, error) {
response := v1beta1.AdmissionResponse{}

// Default response
response.Allowed = true
response.UID = request.UID

// Decode the pod object
var err error
pod := v1.Pod{}
if err := json.Unmarshal(request.Object.Raw, &pod); err != nil {
return response, fmt.Errorf("unable to decode Pod %w", err)
}

// Identify the data classification of the pod, defaulting to unclassified if unset
Expand All @@ -56,15 +58,23 @@ func mutate(request v1beta1.AdmissionRequest, instances map[string][]string) (v1
dataClassification = val
}

if shouldInject {
if shouldInject(&pod) {
patch := v1beta1.PatchTypeJSONPatch
response.PatchType = &patch

response.AuditAnnotations = map[string]string{
"minio-admission-controller": "Added minio credentials",
}

roleName := cleanName("profile-" + pod.Namespace)
// Handle https://github.com/StatCan/aaw-minio-credential-injector/issues/10
var roleName string
if pod.Namespace != "" {
roleName = cleanName("profile-" + pod.Namespace)
} else if request.Namespace != "" {
roleName = cleanName("profile-" + request.Namespace)
} else {
return response, fmt.Errorf("pod and request namespace were empty. Cannot determine the namespace.")
}

patches := []map[string]interface{}{
{
Expand All @@ -86,43 +96,54 @@ func mutate(request v1beta1.AdmissionRequest, instances map[string][]string) (v1
},
}

for _, instance := range instances[dataClassification] {
instanceId := strings.ReplaceAll(instance, "_", "-")
for _, instance := range instances {

// Only apply to the relevant instances
if instance.Classification != dataClassification {
continue
}

instanceId := strings.ReplaceAll(instance.Name, "_", "-")
patches = append(patches, map[string]interface{}{
"op": "add",
"path": fmt.Sprintf("/metadata/annotations/vault.hashicorp.com~1agent-inject-secret-%s", instanceId),
"value": fmt.Sprintf("%s/keys/%s", instance, roleName),
"op": "add",
"path": fmt.Sprintf("/metadata/annotations/vault.hashicorp.com~1agent-inject-secret-%s", instanceId),
"value": fmt.Sprintf("%s/keys/%s", instance.Name, roleName),
})

patches = append(patches, map[string]interface{}{
"op": "add",
"op": "add",
"path": fmt.Sprintf("/metadata/annotations/vault.hashicorp.com~1agent-inject-template-%s", instanceId),
"value": fmt.Sprintf(`
{{- with secret "%s/keys/%s" }}
export MINIO_URL="http://minio.%s-system:443"
export MINIO_URL="%s"
export MINIO_ACCESS_KEY="{{ .Data.accessKeyId }}"
export MINIO_SECRET_KEY="{{ .Data.secretAccessKey }}"
export AWS_ACCESS_KEY_ID="{{ .Data.accessKeyId }}"
export AWS_SECRET_ACCESS_KEY="{{ .Data.secretAccessKey }}"
{{- end }}
`, instance, roleName, instanceId),
`, instance.Name, roleName, instance.ServiceUrl),
})

patches = append(patches, map[string]interface{}{
"op": "add",
"path": fmt.Sprintf("/metadata/annotations/vault.hashicorp.com~1agent-inject-secret-%s.json", instanceId),
"value": fmt.Sprintf("%s/keys/%s", instance, roleName),
"op": "add",
"path": fmt.Sprintf("/metadata/annotations/vault.hashicorp.com~1agent-inject-secret-%s.json", instanceId),
"value": fmt.Sprintf("%s/keys/%s", instance.Name, roleName),
})

patches = append(patches, map[string]interface{}{
"op": "add",
"op": "add",
"path": fmt.Sprintf("/metadata/annotations/vault.hashicorp.com~1agent-inject-template-%s.json", instanceId),
"value": fmt.Sprintf(`
{{- with secret "%s/keys/%s" }}
{"MINIO_URL":"http://minio.%s-system:443","MINIO_ACCESS_KEY":"{{ .Data.accessKeyId }}","MINIO_SECRET_KEY":"{{ .Data.secretAccessKey }}","AWS_ACCESS_KEY_ID":"{{ .Data.accessKeyId }}","AWS_SECRET_ACCESS_KEY":"{{ .Data.secretAccessKey }}"}
{
"MINIO_URL": "%s",
"MINIO_ACCESS_KEY": "{{ .Data.accessKeyId }}",
"MINIO_SECRET_KEY": "{{ .Data.secretAccessKey }}",
"AWS_ACCESS_KEY_ID": "{{ .Data.accessKeyId }}",
"AWS_SECRET_ACCESS_KEY": "{{ .Data.secretAccessKey }}"
}
{{- end }}
`, instance, roleName, instanceId),
`, instance.Name, roleName, instance.ServiceUrl),
})
}

Expand All @@ -135,7 +156,7 @@ export AWS_SECRET_ACCESS_KEY="{{ .Data.secretAccessKey }}"
Status: metav1.StatusSuccess,
}
} else {
log.Printf("Notebook name not found for %s/%s", pod.Namespace, pod.Name)
log.Printf("Not injecting the pod %s/%s", pod.Namespace, pod.Name)
}

return response, nil
Expand Down
5 changes: 4 additions & 1 deletion mutate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,10 @@ func TestMutate(t *testing.T) {
err := json.Unmarshal([]byte(rawJSON), &review)
assert.NoError(t, err, "failed to unmarshal with error %q", err)

response, err := mutate(*review.Request)
// Set the instances
configInstances()

response, err := mutate(*review.Request, instances)
assert.NoError(t, err, "failed to mutate with error %q", err)

log.Println(response)
Expand Down

0 comments on commit 2bb5e5e

Please sign in to comment.