Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 -- <original command>`. You can pass additional flags to `op run` per container using annotations:

| Annotation | Maps to | Notes |
|---|---|---|
| `operator.1password.io/run-env-file.<container-name>` | `--env-file=<value>` | 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 -- <original command>`.

**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:
Expand Down
37 changes: 29 additions & 8 deletions pkg/webhook/webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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{
Expand All @@ -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{
Expand Down Expand Up @@ -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.<container>`.
// 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

Expand Down
85 changes: 85 additions & 0 deletions pkg/webhook/webhook_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down