diff --git a/README.md b/README.md index 3958d22..e945255 100644 --- a/README.md +++ b/README.md @@ -281,6 +281,27 @@ env: value: op://my-vault/my-item/sql/username ``` +## Passing flags to `op run` + +By default the injector rewrites the container command to `op run -- `. You can pass additional flags to `op run` per container using annotations: + +| Annotation | Maps to | Notes | +|---|---|---| +| `operator.1password.io/run-env-file.` | `--env-file=` | Comma-separate paths to repeat the flag (e.g. `"/etc/app.env,/etc/extra.env"`). | + +Example: + +```yaml +# client-deployment.yaml +annotations: + operator.1password.io/inject: "app" + operator.1password.io/run-env-file.app: "/etc/app.env" +``` + +The resulting container command becomes `op run --env-file=/etc/app.env -- `. + +**Note:** the file referenced by `--env-file` must exist inside the container. The injector only adds the `op` binary — you are responsible for mounting the env file (via a `ConfigMap`/`Secret` volume, an init container, or by including it in the image). + ## Troubleshooting If you can't inject secrets in your pod, make sure: diff --git a/pkg/webhook/webhook.go b/pkg/webhook/webhook.go index 64a5a18..1766b59 100644 --- a/pkg/webhook/webhook.go +++ b/pkg/webhook/webhook.go @@ -57,9 +57,10 @@ var ( ) const ( - injectionStatus = "operator.1password.io/status" - injectAnnotation = "operator.1password.io/inject" - versionAnnotation = "operator.1password.io/version" + injectionStatus = "operator.1password.io/status" + injectAnnotation = "operator.1password.io/inject" + versionAnnotation = "operator.1password.io/version" + runEnvFileAnnotation = "operator.1password.io/run-env-file" ) type SecretInjector struct { @@ -233,7 +234,7 @@ func (s *SecretInjector) mutate(ar *admissionv1.AdmissionReview) *admissionv1.Ad if !mutate { continue } - didMutate, initContainerPatch, err := s.mutateContainer(ctx, &c, i) + didMutate, initContainerPatch, err := s.mutateContainer(ctx, &c, i, pod.Annotations) if err != nil { return &admissionv1.AdmissionResponse{ Result: &metav1.Status{ @@ -254,7 +255,7 @@ func (s *SecretInjector) mutate(ar *admissionv1.AdmissionReview) *admissionv1.Ad continue } - didMutate, containerPatch, err := s.mutateContainer(ctx, &c, i) + didMutate, containerPatch, err := s.mutateContainer(ctx, &c, i, pod.Annotations) if err != nil { glog.Error("Error occurred mutating container for secret injection: ", err) return &admissionv1.AdmissionResponse{ @@ -386,15 +387,35 @@ func passUserAgentInformationToCLI(container *corev1.Container, containerIndex i return setEnvironment(*container, containerIndex, userAgentEnvs, "/spec/containers") } +// buildOpRunFlags returns extra flags to pass to `op run` for the given container, +// derived from per-container annotations like `operator.1password.io/run-env-file.`. +// Values may be comma-separated to repeat the flag (e.g. multiple --env-file paths). +func buildOpRunFlags(annotations map[string]string, containerName string) []string { + var flags []string + if v, ok := annotations[runEnvFileAnnotation+"."+containerName]; ok { + for _, path := range strings.Split(v, ",") { + path = strings.TrimSpace(path) + if path == "" { + continue + } + flags = append(flags, "--env-file="+path) + } + } + return flags +} + // mutates the container to allow for secrets to be injected into the container via the op cli -func (s *SecretInjector) mutateContainer(cxt context.Context, container *corev1.Container, containerIndex int) (bool, []patchOperation, error) { +func (s *SecretInjector) mutateContainer(cxt context.Context, container *corev1.Container, containerIndex int, podAnnotations map[string]string) (bool, []patchOperation, error) { // prepending op run command to the container command so that secrets are injected before the main process is started if len(container.Command) == 0 { return false, nil, fmt.Errorf("not attaching OP to the container %s: the podspec does not define a command", container.Name) } - // Prepend the command with op run -- - container.Command = append([]string{binVolumeMountPath + "op", "run", "--"}, container.Command...) + // Prepend the command with op run [flags] -- + runFlags := buildOpRunFlags(podAnnotations, container.Name) + prefix := append([]string{binVolumeMountPath + "op", "run"}, runFlags...) + prefix = append(prefix, "--") + container.Command = append(prefix, container.Command...) var patch []patchOperation diff --git a/pkg/webhook/webhook_test.go b/pkg/webhook/webhook_test.go index 041a228..ece13b8 100644 --- a/pkg/webhook/webhook_test.go +++ b/pkg/webhook/webhook_test.go @@ -279,6 +279,91 @@ var _ = Describe("Webhook Test", Ordered, func() { Expect(patched.Annotations).To(HaveKeyWithValue("myannotation", "mine")) }) + It("injects --env-file flag from per-container run-env-file annotation", func() { + pod := corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + "operator.1password.io/inject": "app", + "operator.1password.io/run-env-file.app": "/etc/app.env", + }, + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + {Name: "app", Command: []string{"sleep", "infinity"}}, + }, + }, + } + raw, err := json.Marshal(pod) + Expect(err).NotTo(HaveOccurred()) + responseBody := sendPodAndGetResponse(pod, rr, handler) + Expect(responseBody.Patch).NotTo(BeNil()) + patch, err := jsonpatch.DecodePatch(responseBody.Patch) + Expect(err).NotTo(HaveOccurred()) + patchedRaw, err := patch.Apply(raw) + Expect(err).NotTo(HaveOccurred()) + var patched corev1.Pod + Expect(json.Unmarshal(patchedRaw, &patched)).To(Succeed()) + Expect(patched.Spec.Containers[0].Command).To(Equal([]string{ + "/op/bin/op", "run", "--env-file=/etc/app.env", "--", "sleep", "infinity", + })) + }) + + It("injects multiple --env-file flags when annotation value is comma-separated", func() { + pod := corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + "operator.1password.io/inject": "app", + "operator.1password.io/run-env-file.app": "/etc/app.env, /etc/extra.env", + }, + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + {Name: "app", Command: []string{"sleep", "infinity"}}, + }, + }, + } + raw, err := json.Marshal(pod) + Expect(err).NotTo(HaveOccurred()) + responseBody := sendPodAndGetResponse(pod, rr, handler) + patch, err := jsonpatch.DecodePatch(responseBody.Patch) + Expect(err).NotTo(HaveOccurred()) + patchedRaw, err := patch.Apply(raw) + Expect(err).NotTo(HaveOccurred()) + var patched corev1.Pod + Expect(json.Unmarshal(patchedRaw, &patched)).To(Succeed()) + Expect(patched.Spec.Containers[0].Command).To(Equal([]string{ + "/op/bin/op", "run", "--env-file=/etc/app.env", "--env-file=/etc/extra.env", "--", "sleep", "infinity", + })) + }) + + It("does not add --env-file when run-env-file annotation targets a different container", func() { + pod := corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + "operator.1password.io/inject": "app", + "operator.1password.io/run-env-file.other": "/etc/other.env", + }, + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + {Name: "app", Command: []string{"sleep", "infinity"}}, + }, + }, + } + raw, err := json.Marshal(pod) + Expect(err).NotTo(HaveOccurred()) + responseBody := sendPodAndGetResponse(pod, rr, handler) + patch, err := jsonpatch.DecodePatch(responseBody.Patch) + Expect(err).NotTo(HaveOccurred()) + patchedRaw, err := patch.Apply(raw) + Expect(err).NotTo(HaveOccurred()) + var patched corev1.Pod + Expect(json.Unmarshal(patchedRaw, &patched)).To(Succeed()) + Expect(patched.Spec.Containers[0].Command).To(Equal([]string{ + "/op/bin/op", "run", "--", "sleep", "infinity", + })) + }) + It("replaces status when the key already exists (empty value)", func() { pod := corev1.Pod{ ObjectMeta: metav1.ObjectMeta{