From 3a1d9c8b288818c551274160c6e10c21e16a252e Mon Sep 17 00:00:00 2001 From: David Solc Date: Tue, 3 Feb 2026 16:32:49 +0100 Subject: [PATCH 1/8] feat: add kubernetes and sops plugins Kubernetes plugin: - kubectl, helm, stern, k9s support - Base64-encoded kubeconfig stored in 1Password SOPS plugin: - sops, helm support - Age secret key provisioning --- .pre-commit-config.yaml | 1 + plugins/kubernetes/k9s.go | 23 ++++ plugins/kubernetes/kubeconfig.go | 56 ++++++++++ plugins/kubernetes/kubeconfig_test.go | 54 ++++++++++ plugins/kubernetes/kubectl.go | 24 +++++ plugins/kubernetes/plugin.go | 24 +++++ plugins/kubernetes/stern.go | 23 ++++ plugins/kubernetes/test-fixtures/config | 19 ++++ plugins/plugins.go | 135 ++++++++++++++++++++++++ plugins/sops/age_secret_key.go | 40 +++++++ plugins/sops/age_secret_key_test.go | 41 +++++++ plugins/sops/helm.go | 27 +++++ plugins/sops/plugin.go | 23 ++++ plugins/sops/sops.go | 25 +++++ 14 files changed, 515 insertions(+) create mode 120000 .pre-commit-config.yaml create mode 100644 plugins/kubernetes/k9s.go create mode 100644 plugins/kubernetes/kubeconfig.go create mode 100644 plugins/kubernetes/kubeconfig_test.go create mode 100644 plugins/kubernetes/kubectl.go create mode 100644 plugins/kubernetes/plugin.go create mode 100644 plugins/kubernetes/stern.go create mode 100644 plugins/kubernetes/test-fixtures/config create mode 100644 plugins/plugins.go create mode 100644 plugins/sops/age_secret_key.go create mode 100644 plugins/sops/age_secret_key_test.go create mode 100644 plugins/sops/helm.go create mode 100644 plugins/sops/plugin.go create mode 100644 plugins/sops/sops.go diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 120000 index 00000000..2cd137d7 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1 @@ +/nix/store/davrh460p36m2bpnchpfb2majib7cag0-pre-commit-config.json \ No newline at end of file diff --git a/plugins/kubernetes/k9s.go b/plugins/kubernetes/k9s.go new file mode 100644 index 00000000..6ec7f942 --- /dev/null +++ b/plugins/kubernetes/k9s.go @@ -0,0 +1,23 @@ +package kubernetes + +import ( + "github.com/1Password/shell-plugins/sdk" + "github.com/1Password/shell-plugins/sdk/needsauth" + "github.com/1Password/shell-plugins/sdk/schema" +) + +func K9sCLI() schema.Executable { + return schema.Executable{ + Name: "k9s", + Runs: []string{"k9s"}, + DocsURL: sdk.URL("https://k9scli.io/"), + NeedsAuth: needsauth.IfAll( + needsauth.NotForHelpOrVersion(), + ), + Uses: []schema.CredentialUsage{ + { + Name: sdk.CredentialName("Kubeconfig"), + }, + }, + } +} diff --git a/plugins/kubernetes/kubeconfig.go b/plugins/kubernetes/kubeconfig.go new file mode 100644 index 00000000..279447b6 --- /dev/null +++ b/plugins/kubernetes/kubeconfig.go @@ -0,0 +1,56 @@ +package kubernetes + +import ( + "context" + "encoding/base64" + + "github.com/1Password/shell-plugins/sdk" + "github.com/1Password/shell-plugins/sdk/importer" + "github.com/1Password/shell-plugins/sdk/provision" + "github.com/1Password/shell-plugins/sdk/schema" + "github.com/1Password/shell-plugins/sdk/schema/fieldname" +) + +func Kubeconfig() schema.CredentialType { + return schema.CredentialType{ + Name: sdk.CredentialName("Kubeconfig"), + DocsURL: sdk.URL("https://kubernetes.io/docs/concepts/configuration/organize-cluster-access-kubeconfig/"), + Fields: []schema.CredentialField{ + { + Name: fieldname.Credential, + MarkdownDescription: "Base64-encoded kubeconfig YAML file contents.", + Secret: true, + }, + }, + DefaultProvisioner: provision.TempFile( + kubeconfigFileContents, + provision.Filename("config"), + provision.SetPathAsEnvVar("KUBECONFIG"), + ), + Importer: importer.TryAll( + TryKubeconfigFile(), + ), + } +} + +func kubeconfigFileContents(in sdk.ProvisionInput) ([]byte, error) { + encoded := in.ItemFields[fieldname.Credential] + decoded, err := base64.StdEncoding.DecodeString(encoded) + if err != nil { + return nil, err + } + return decoded, nil +} + +func TryKubeconfigFile() sdk.Importer { + return importer.TryFile("~/.kube/config", func(ctx context.Context, contents importer.FileContents, in sdk.ImportInput, out *sdk.ImportAttempt) { + // Import existing kubeconfig as base64-encoded + encoded := base64.StdEncoding.EncodeToString(contents) + out.AddCandidate(sdk.ImportCandidate{ + Fields: map[sdk.FieldName]string{ + fieldname.Credential: encoded, + }, + NameHint: importer.SanitizeNameHint("default"), + }) + }) +} diff --git a/plugins/kubernetes/kubeconfig_test.go b/plugins/kubernetes/kubeconfig_test.go new file mode 100644 index 00000000..d17aca40 --- /dev/null +++ b/plugins/kubernetes/kubeconfig_test.go @@ -0,0 +1,54 @@ +package kubernetes + +import ( + "encoding/base64" + "testing" + + "github.com/1Password/shell-plugins/sdk" + "github.com/1Password/shell-plugins/sdk/plugintest" + "github.com/1Password/shell-plugins/sdk/schema/fieldname" +) + +func TestKubeconfigProvisioner(t *testing.T) { + rawConfig := plugintest.LoadFixture(t, "config") + encodedConfig := base64.StdEncoding.EncodeToString([]byte(rawConfig)) + + plugintest.TestProvisioner(t, Kubeconfig().DefaultProvisioner, map[string]plugintest.ProvisionCase{ + "default": { + ItemFields: map[sdk.FieldName]string{ + fieldname.Credential: encodedConfig, + }, + ExpectedOutput: sdk.ProvisionOutput{ + Environment: map[string]string{ + "KUBECONFIG": "/tmp/config", + }, + Files: map[string]sdk.OutputFile{ + "/tmp/config": { + Contents: []byte(rawConfig), + }, + }, + }, + }, + }) +} + +func TestKubeconfigImporter(t *testing.T) { + rawConfig := plugintest.LoadFixture(t, "config") + encodedConfig := base64.StdEncoding.EncodeToString([]byte(rawConfig)) + + plugintest.TestImporter(t, Kubeconfig().Importer, map[string]plugintest.ImportCase{ + "kubeconfig file": { + Files: map[string]string{ + "~/.kube/config": rawConfig, + }, + ExpectedCandidates: []sdk.ImportCandidate{ + { + Fields: map[sdk.FieldName]string{ + fieldname.Credential: encodedConfig, + }, + NameHint: "", + }, + }, + }, + }) +} diff --git a/plugins/kubernetes/kubectl.go b/plugins/kubernetes/kubectl.go new file mode 100644 index 00000000..f579e9f9 --- /dev/null +++ b/plugins/kubernetes/kubectl.go @@ -0,0 +1,24 @@ +package kubernetes + +import ( + "github.com/1Password/shell-plugins/sdk" + "github.com/1Password/shell-plugins/sdk/needsauth" + "github.com/1Password/shell-plugins/sdk/schema" +) + +func KubectlCLI() schema.Executable { + return schema.Executable{ + Name: "kubectl", + Runs: []string{"kubectl"}, + DocsURL: sdk.URL("https://kubernetes.io/docs/reference/kubectl/"), + NeedsAuth: needsauth.IfAll( + needsauth.NotForHelpOrVersion(), + needsauth.NotWithoutArgs(), + ), + Uses: []schema.CredentialUsage{ + { + Name: sdk.CredentialName("Kubeconfig"), + }, + }, + } +} diff --git a/plugins/kubernetes/plugin.go b/plugins/kubernetes/plugin.go new file mode 100644 index 00000000..648e6f9f --- /dev/null +++ b/plugins/kubernetes/plugin.go @@ -0,0 +1,24 @@ +package kubernetes + +import ( + "github.com/1Password/shell-plugins/sdk" + "github.com/1Password/shell-plugins/sdk/schema" +) + +func New() schema.Plugin { + return schema.Plugin{ + Name: "kubernetes", + Platform: schema.PlatformInfo{ + Name: "Kubernetes", + Homepage: sdk.URL("https://kubernetes.io"), + }, + Credentials: []schema.CredentialType{ + Kubeconfig(), + }, + Executables: []schema.Executable{ + KubectlCLI(), + SternCLI(), + K9sCLI(), + }, + } +} diff --git a/plugins/kubernetes/stern.go b/plugins/kubernetes/stern.go new file mode 100644 index 00000000..07ccb99e --- /dev/null +++ b/plugins/kubernetes/stern.go @@ -0,0 +1,23 @@ +package kubernetes + +import ( + "github.com/1Password/shell-plugins/sdk" + "github.com/1Password/shell-plugins/sdk/needsauth" + "github.com/1Password/shell-plugins/sdk/schema" +) + +func SternCLI() schema.Executable { + return schema.Executable{ + Name: "stern", + Runs: []string{"stern"}, + DocsURL: sdk.URL("https://github.com/stern/stern"), + NeedsAuth: needsauth.IfAll( + needsauth.NotForHelpOrVersion(), + ), + Uses: []schema.CredentialUsage{ + { + Name: sdk.CredentialName("Kubeconfig"), + }, + }, + } +} diff --git a/plugins/kubernetes/test-fixtures/config b/plugins/kubernetes/test-fixtures/config new file mode 100644 index 00000000..10f840aa --- /dev/null +++ b/plugins/kubernetes/test-fixtures/config @@ -0,0 +1,19 @@ +apiVersion: v1 +kind: Config +clusters: +- cluster: + certificate-authority-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUJlRENDQVIyZ0F3SUJBZ0lCQURBS0JnZ3Foa2pPUFFRREFqQWpNU0V3SHdZRFZRUUREQmhyTTNNdGMyVnkKZG1WeUxXTmhRREUzTURBeU16TTJOREV3SGhjTk1qTXhNVEU1TVRBeU56SXhXaGNOTXpNeE1URTJNVEF5TnpJeApXakFqTVNFd0h3WURWUVFEREJock0zTXRjMlZ5ZG1WeUxXTmhRREUzTURBeU16TTJOREV3V1RBVEJnY3Foa2pPClBRSUJCZ2dxaGtqT1BRTUJCd05DQUFUMUZiY29ycjhBQ1NkcmNqQk5xQ3JSa0M2Y2tMYmZUQ0NSSUN2UndhN2IKUjZiNUxRZHZuOXZVYkhiVHN4Z3FJQ2pYYjZRTEtIRTRrQXlQQ3BDNnBPUEpvMEl3UURBT0JnTlZIUThCQWY4RQpCQU1DQXFRd0R3WURWUjBUQVFIL0JBVXdBd0VCL3pBZEJnTlZIUTRFRmdRVWVlUllndjZBbGFCNVc0dDV0Rm9ZCm9OSHVkVDB3Q2dZSUtvWkl6ajBFQXdJRFNRQXdSZ0loQUk0TlQyR3hxNHYwZExHRlFkNkdtbHdaZ0dBckVZRWoKL1FkeWZsOU12QTZyQWlFQWljVjdiVzlqcVJwNjh4cHdJeDlYak9OeUt2V3dZMVdQVWYzQU1kQTd6T2s9Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K + server: https://10.0.0.1:6443 + name: default +contexts: +- context: + cluster: default + user: default + name: default +current-context: default +preferences: {} +users: +- name: default + user: + client-certificate-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUJrVENDQVRlZ0F3SUJBZ0lJVHVNdTNtNE8waTh3Q2dZSUtvWkl6ajBFQXdJd0l6RWhNQjhHQTFVRUF3d1kKYXpOekxXTnNhV1Z1ZEMxallVQXhOekF3TWpNek5qUXhNQjRYRFRJek1URXhPVEV3TWpjeU1Wb1hEVEkwTVRFeApPREV3TWpjeU1Wb3dNREVYTUJVR0ExVUVDaE1PYzNsemRHVnRPbTFoYzNSbGNuTXhGVEFUQmdOVkJBTVRESE41CmMzUmxiVHBoWkcxcGJqQlpNQk1HQnlxR1NNNDlBZ0VHQ0NxR1NNNDlBd0VIQTBJQUJNL0hvRHZUZmg2dEtoVVUKM3lHZHBrU05EWVhGWjBYYjVVMHRKRzh4TlFiQmEzaDhRQjJMMVN0MDVNRlQ2dEhxUWpsUkJ6cG1qRnJ2L1haQQp1RUl4WmRlalNEQkdNQTRHQTFVZER3RUIvd1FFQXdJRm9EQVRCZ05WSFNVRUREQUtCZ2dyQmdFRkJRY0RBakFmCkJnTlZIU01FR0RBV2dCUmYwYnh0NTVSdlE4M3pxMGxrdnRXU28zYUNDREFLQmdncWhrak9QUVFEQWdOSUFEQkYKQWlFQXo1K2txTWJpMjJKMW1LNGFldlVYUVFGVm9hSXFWRFBzTjZHcW1iZGxaRWtDSUJqeWVxc0tkL0VxdFBXbgpaSXRCL21STC9WdDRHRUhJbGpUckxLNytlN2ZOCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0KLS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUJkekNDQVIyZ0F3SUJBZ0lCQURBS0JnZ3Foa2pPUFFRREFqQWpNU0V3SHdZRFZRUUREQmhyTTNNdFkyeHAKWlc1MExXTmhRREUzTURBeU16TTJOREV3SGhjTk1qTXhNVEU1TVRBeU56SXhXaGNOTXpNeE1URTJNVEF5TnpJeApXakFqTVNFd0h3WURWUVFEREJock0zTXRZMnhwWlc1MExXTmhRREUzTURBeU16TTJOREV3V1RBVEJnY3Foa2pPClBRSUJCZ2dxaGtqT1BRTUJCd05DQUFSMU9ucCsxUnpLYVVVN0lJNTVhTXZJRUFXeE9udmNqd0E0NnRZSDQ3amkKb0dIVFViTHJXc3ZxMHFZeEpJZlkzRmdZekZYMVlkLzhBbVJxWGJrTldkUW9vMEl3UURBT0JnTlZIUThCQWY4RQpCQU1DQXFRd0R3WURWUjBUQVFIL0JBVXdBd0VCL3pBZEJnTlZIUTRFRmdRVVg5RzhiZWVVYjBQTjg2dEpaTC9WCmtxTjJnZ2d3Q2dZSUtvWkl6ajBFQXdJRFNBQXdSUUloQUxoSER3UlllVXBoS0ZSOURKZ2EycjlLRlBmQ2dLSnUKd0JOK2ptOW4wdm1YQWlBenM5Q0hDN1F5aElFSnNmVjVLaGF1MHhqQ1pYdEJpblQ5aEt4d0RGMTJGUT09Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K + client-key-data: LS0tLS1CRUdJTiBFQyBQUklWQVRFIEtFWS0tLS0tCk1IUUNBUUVFSUFTbE5FV3lPRWRuSWxYVWpRdG1Pc1cwWjdwYWxpTUlKdENMcmhKSGZRYlBvQWNHQlN1QkJBQUsKb1VRRFFnQUV6OGVnTzlOK0hxMHFGUlRmSVoybVJJME5oY1ZuUmR2bFRTMGtieksxQnNGcmVIeEFIWXZWSzNUawp3VlBxMGVwQ09WRUhPbWFNV3UvOWRrQzRRakZsMXc9PQotLS0tLUVORCBFQyBQUklWQVRFIEtFWS0tLS0tCg== diff --git a/plugins/plugins.go b/plugins/plugins.go new file mode 100644 index 00000000..f7290a48 --- /dev/null +++ b/plugins/plugins.go @@ -0,0 +1,135 @@ +package plugins + +// This file gets auto-generated by the "make registry" command, so should not be edited by hand. + +import ( + "github.com/1Password/shell-plugins/plugins/akamai" + "github.com/1Password/shell-plugins/plugins/argocd" + "github.com/1Password/shell-plugins/plugins/atlas" + "github.com/1Password/shell-plugins/plugins/aws" + "github.com/1Password/shell-plugins/plugins/axiom" + "github.com/1Password/shell-plugins/plugins/binance" + "github.com/1Password/shell-plugins/plugins/cachix" + "github.com/1Password/shell-plugins/plugins/cargo" + "github.com/1Password/shell-plugins/plugins/circleci" + "github.com/1Password/shell-plugins/plugins/civo" + "github.com/1Password/shell-plugins/plugins/confluent" + "github.com/1Password/shell-plugins/plugins/crowdin" + "github.com/1Password/shell-plugins/plugins/databricks" + "github.com/1Password/shell-plugins/plugins/datadog" + "github.com/1Password/shell-plugins/plugins/digitalocean" + "github.com/1Password/shell-plugins/plugins/fastly" + "github.com/1Password/shell-plugins/plugins/flyctl" + "github.com/1Password/shell-plugins/plugins/fossa" + "github.com/1Password/shell-plugins/plugins/gitea" + "github.com/1Password/shell-plugins/plugins/github" + "github.com/1Password/shell-plugins/plugins/gitlab" + "github.com/1Password/shell-plugins/plugins/hcloud" + "github.com/1Password/shell-plugins/plugins/heroku" + "github.com/1Password/shell-plugins/plugins/homebrew" + "github.com/1Password/shell-plugins/plugins/huggingface" + "github.com/1Password/shell-plugins/plugins/influxdb" + "github.com/1Password/shell-plugins/plugins/kaggle" + "github.com/1Password/shell-plugins/plugins/kubernetes" + "github.com/1Password/shell-plugins/plugins/lacework" + "github.com/1Password/shell-plugins/plugins/laravelforge" + "github.com/1Password/shell-plugins/plugins/laravelvapor" + "github.com/1Password/shell-plugins/plugins/linode" + "github.com/1Password/shell-plugins/plugins/localstack" + "github.com/1Password/shell-plugins/plugins/mysql" + "github.com/1Password/shell-plugins/plugins/ngrok" + "github.com/1Password/shell-plugins/plugins/ohdear" + "github.com/1Password/shell-plugins/plugins/okta" + "github.com/1Password/shell-plugins/plugins/openai" + "github.com/1Password/shell-plugins/plugins/pipedream" + "github.com/1Password/shell-plugins/plugins/postgresql" + "github.com/1Password/shell-plugins/plugins/pulumi" + "github.com/1Password/shell-plugins/plugins/readme" + "github.com/1Password/shell-plugins/plugins/scaleway" + "github.com/1Password/shell-plugins/plugins/sentry" + "github.com/1Password/shell-plugins/plugins/snowflake" + "github.com/1Password/shell-plugins/plugins/snyk" + "github.com/1Password/shell-plugins/plugins/sops" + "github.com/1Password/shell-plugins/plugins/sourcegraph" + "github.com/1Password/shell-plugins/plugins/stripe" + "github.com/1Password/shell-plugins/plugins/terraform" + "github.com/1Password/shell-plugins/plugins/todoist" + "github.com/1Password/shell-plugins/plugins/treasuredata" + "github.com/1Password/shell-plugins/plugins/tugboat" + "github.com/1Password/shell-plugins/plugins/twilio" + "github.com/1Password/shell-plugins/plugins/upstash" + "github.com/1Password/shell-plugins/plugins/vault" + "github.com/1Password/shell-plugins/plugins/vercel" + "github.com/1Password/shell-plugins/plugins/vertica" + "github.com/1Password/shell-plugins/plugins/vultr" + "github.com/1Password/shell-plugins/plugins/wrangler" + "github.com/1Password/shell-plugins/plugins/yugabytedb" + "github.com/1Password/shell-plugins/plugins/zapier" + "github.com/1Password/shell-plugins/plugins/zendesk" +) + +func init() { + Register(akamai.New()) + Register(argocd.New()) + Register(atlas.New()) + Register(aws.New()) + Register(axiom.New()) + Register(binance.New()) + Register(cachix.New()) + Register(cargo.New()) + Register(circleci.New()) + Register(civo.New()) + Register(confluent.New()) + Register(crowdin.New()) + Register(databricks.New()) + Register(datadog.New()) + Register(digitalocean.New()) + Register(fastly.New()) + Register(flyctl.New()) + Register(fossa.New()) + Register(gitea.New()) + Register(github.New()) + Register(gitlab.New()) + Register(hcloud.New()) + Register(heroku.New()) + Register(homebrew.New()) + Register(huggingface.New()) + Register(influxdb.New()) + Register(kaggle.New()) + Register(kubernetes.New()) + Register(lacework.New()) + Register(laravelforge.New()) + Register(laravelvapor.New()) + Register(linode.New()) + Register(localstack.New()) + Register(mysql.New()) + Register(ngrok.New()) + Register(ohdear.New()) + Register(okta.New()) + Register(openai.New()) + Register(pipedream.New()) + Register(postgresql.New()) + Register(pulumi.New()) + Register(readme.New()) + Register(scaleway.New()) + Register(sentry.New()) + Register(snowflake.New()) + Register(snyk.New()) + Register(sops.New()) + Register(sourcegraph.New()) + Register(stripe.New()) + Register(terraform.New()) + Register(todoist.New()) + Register(treasuredata.New()) + Register(tugboat.New()) + Register(twilio.New()) + Register(upstash.New()) + Register(vault.New()) + Register(vercel.New()) + Register(vertica.New()) + Register(vultr.New()) + Register(wrangler.New()) + Register(yugabytedb.New()) + Register(zapier.New()) + Register(zendesk.New()) +} diff --git a/plugins/sops/age_secret_key.go b/plugins/sops/age_secret_key.go new file mode 100644 index 00000000..7c755561 --- /dev/null +++ b/plugins/sops/age_secret_key.go @@ -0,0 +1,40 @@ +package sops + +import ( + "github.com/1Password/shell-plugins/sdk" + "github.com/1Password/shell-plugins/sdk/importer" + "github.com/1Password/shell-plugins/sdk/provision" + "github.com/1Password/shell-plugins/sdk/schema" + "github.com/1Password/shell-plugins/sdk/schema/credname" + "github.com/1Password/shell-plugins/sdk/schema/fieldname" +) + +func AgeSecretKey() schema.CredentialType { + return schema.CredentialType{ + Name: credname.SecretKey, + DocsURL: sdk.URL("https://github.com/getsops/sops#encrypting-using-age"), + ManagementURL: nil, + Fields: []schema.CredentialField{ + { + Name: fieldname.PrivateKey, + MarkdownDescription: "Age secret key used by SOPS for encryption and decryption.", + Secret: true, + Composition: &schema.ValueComposition{ + Prefix: "AGE-SECRET-KEY-", + Charset: schema.Charset{ + Uppercase: true, + Digits: true, + }, + }, + }, + }, + DefaultProvisioner: provision.EnvVars(defaultEnvVarMapping), + Importer: importer.TryAll( + importer.TryEnvVarPair(defaultEnvVarMapping), + ), + } +} + +var defaultEnvVarMapping = map[string]sdk.FieldName{ + "SOPS_AGE_KEY": fieldname.PrivateKey, +} diff --git a/plugins/sops/age_secret_key_test.go b/plugins/sops/age_secret_key_test.go new file mode 100644 index 00000000..506aa53e --- /dev/null +++ b/plugins/sops/age_secret_key_test.go @@ -0,0 +1,41 @@ +package sops + +import ( + "testing" + + "github.com/1Password/shell-plugins/sdk" + "github.com/1Password/shell-plugins/sdk/plugintest" + "github.com/1Password/shell-plugins/sdk/schema/fieldname" +) + +func TestAgeSecretKeyImporter(t *testing.T) { + plugintest.TestImporter(t, AgeSecretKey().Importer, map[string]plugintest.ImportCase{ + "environment": { + Environment: map[string]string{ + "SOPS_AGE_KEY": "AGE-SECRET-KEY-1QFWENTHXAAPACFPMXHQCREP64GJE5YTHXLX0RPFSXRSPDJGCR0SSWYNX3D", + }, + ExpectedCandidates: []sdk.ImportCandidate{ + { + Fields: map[sdk.FieldName]string{ + fieldname.PrivateKey: "AGE-SECRET-KEY-1QFWENTHXAAPACFPMXHQCREP64GJE5YTHXLX0RPFSXRSPDJGCR0SSWYNX3D", + }, + }, + }, + }, + }) +} + +func TestAgeSecretKeyProvisioner(t *testing.T) { + plugintest.TestProvisioner(t, AgeSecretKey().DefaultProvisioner, map[string]plugintest.ProvisionCase{ + "default": { + ItemFields: map[sdk.FieldName]string{ + fieldname.PrivateKey: "AGE-SECRET-KEY-1QFWENTHXAAPACFPMXHQCREP64GJE5YTHXLX0RPFSXRSPDJGCR0SSWYNX3D", + }, + ExpectedOutput: sdk.ProvisionOutput{ + Environment: map[string]string{ + "SOPS_AGE_KEY": "AGE-SECRET-KEY-1QFWENTHXAAPACFPMXHQCREP64GJE5YTHXLX0RPFSXRSPDJGCR0SSWYNX3D", + }, + }, + }, + }) +} diff --git a/plugins/sops/helm.go b/plugins/sops/helm.go new file mode 100644 index 00000000..d946cbc5 --- /dev/null +++ b/plugins/sops/helm.go @@ -0,0 +1,27 @@ +package sops + +import ( + "github.com/1Password/shell-plugins/sdk" + "github.com/1Password/shell-plugins/sdk/needsauth" + "github.com/1Password/shell-plugins/sdk/schema" + "github.com/1Password/shell-plugins/sdk/schema/credname" +) + +func HelmCLI() schema.Executable { + return schema.Executable{ + Name: "Helm with SOPS Secrets", + Runs: []string{"helm"}, + DocsURL: sdk.URL("https://github.com/jkroepke/helm-secrets"), + NeedsAuth: needsauth.IfAll( + needsauth.NotForHelpOrVersion(), + needsauth.NotWithoutArgs(), + // Only authenticate when using helm-secrets plugin + needsauth.ForCommand("secrets"), + ), + Uses: []schema.CredentialUsage{ + { + Name: credname.SecretKey, + }, + }, + } +} diff --git a/plugins/sops/plugin.go b/plugins/sops/plugin.go new file mode 100644 index 00000000..525b0373 --- /dev/null +++ b/plugins/sops/plugin.go @@ -0,0 +1,23 @@ +package sops + +import ( + "github.com/1Password/shell-plugins/sdk" + "github.com/1Password/shell-plugins/sdk/schema" +) + +func New() schema.Plugin { + return schema.Plugin{ + Name: "sops", + Platform: schema.PlatformInfo{ + Name: "SOPS", + Homepage: sdk.URL("https://github.com/getsops/sops"), + }, + Credentials: []schema.CredentialType{ + AgeSecretKey(), + }, + Executables: []schema.Executable{ + SOPSCLI(), + HelmCLI(), + }, + } +} diff --git a/plugins/sops/sops.go b/plugins/sops/sops.go new file mode 100644 index 00000000..0a59d5e4 --- /dev/null +++ b/plugins/sops/sops.go @@ -0,0 +1,25 @@ +package sops + +import ( + "github.com/1Password/shell-plugins/sdk" + "github.com/1Password/shell-plugins/sdk/needsauth" + "github.com/1Password/shell-plugins/sdk/schema" + "github.com/1Password/shell-plugins/sdk/schema/credname" +) + +func SOPSCLI() schema.Executable { + return schema.Executable{ + Name: "SOPS CLI", + Runs: []string{"sops"}, + DocsURL: sdk.URL("https://github.com/getsops/sops"), + NeedsAuth: needsauth.IfAll( + needsauth.NotForHelpOrVersion(), + needsauth.NotWithoutArgs(), + ), + Uses: []schema.CredentialUsage{ + { + Name: credname.SecretKey, + }, + }, + } +} From 00b79f0fcbef8293b29be215e3b2c4d94e0ef889 Mon Sep 17 00:00:00 2001 From: David Solc Date: Wed, 4 Feb 2026 16:29:27 +0100 Subject: [PATCH 2/8] feat(sops): add kubernetes credential to helm executable Helm with helm-secrets plugin now uses both SOPS and Kubernetes credentials via cross-plugin reference. This enables helm to: - Decrypt secrets using SOPS age key (SOPS_AGE_KEY env var) - Connect to Kubernetes clusters (KUBECONFIG env var) Removed ForCommand("secrets") auth restriction since helm always needs kubeconfig for cluster operations. --- plugins/sops/helm.go | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/plugins/sops/helm.go b/plugins/sops/helm.go index d946cbc5..d6cd219a 100644 --- a/plugins/sops/helm.go +++ b/plugins/sops/helm.go @@ -9,18 +9,22 @@ import ( func HelmCLI() schema.Executable { return schema.Executable{ - Name: "Helm with SOPS Secrets", + Name: "Helm with SOPS Secrets and Kubernetes", Runs: []string{"helm"}, DocsURL: sdk.URL("https://github.com/jkroepke/helm-secrets"), NeedsAuth: needsauth.IfAll( needsauth.NotForHelpOrVersion(), needsauth.NotWithoutArgs(), - // Only authenticate when using helm-secrets plugin - needsauth.ForCommand("secrets"), ), Uses: []schema.CredentialUsage{ { Name: credname.SecretKey, + // SOPS age key - provisions SOPS_AGE_KEY env var + }, + { + Name: sdk.CredentialName("Kubeconfig"), + Plugin: "kubernetes", + // Kubeconfig - provisions KUBECONFIG env var pointing to temp file }, }, } From bc44ddaabb49c72fced0a92077acb65343b3c847 Mon Sep 17 00:00:00 2001 From: David Solc Date: Mon, 9 Feb 2026 02:31:13 +0100 Subject: [PATCH 3/8] feat(sops): add helmfile executable for SOPS secret decryption Helmfile transparently calls helm-secrets during sync/lint/apply, so it needs the SOPS_AGE_KEY injected via op plugin run. --- plugins/sops/helmfile.go | 25 +++++++++++++++++++++++++ plugins/sops/plugin.go | 1 + 2 files changed, 26 insertions(+) create mode 100644 plugins/sops/helmfile.go diff --git a/plugins/sops/helmfile.go b/plugins/sops/helmfile.go new file mode 100644 index 00000000..cb9aed37 --- /dev/null +++ b/plugins/sops/helmfile.go @@ -0,0 +1,25 @@ +package sops + +import ( + "github.com/1Password/shell-plugins/sdk" + "github.com/1Password/shell-plugins/sdk/needsauth" + "github.com/1Password/shell-plugins/sdk/schema" + "github.com/1Password/shell-plugins/sdk/schema/credname" +) + +func HelmfileCLI() schema.Executable { + return schema.Executable{ + Name: "Helmfile with SOPS Secrets", + Runs: []string{"helmfile"}, + DocsURL: sdk.URL("https://github.com/helmfile/helmfile"), + NeedsAuth: needsauth.IfAll( + needsauth.NotForHelpOrVersion(), + needsauth.NotWithoutArgs(), + ), + Uses: []schema.CredentialUsage{ + { + Name: credname.SecretKey, + }, + }, + } +} diff --git a/plugins/sops/plugin.go b/plugins/sops/plugin.go index 525b0373..c2613598 100644 --- a/plugins/sops/plugin.go +++ b/plugins/sops/plugin.go @@ -18,6 +18,7 @@ func New() schema.Plugin { Executables: []schema.Executable{ SOPSCLI(), HelmCLI(), + HelmfileCLI(), }, } } From e76631814671206df6d11b942d8b53d27d32232a Mon Sep 17 00:00:00 2001 From: David Solc Date: Tue, 10 Feb 2026 13:25:35 +0100 Subject: [PATCH 4/8] feat(sops): make SOPS age key optional for helm executable Kubeconfig is the primary credential; SOPS age key is only needed when using helm-secrets plugin. --- plugins/sops/helm.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/plugins/sops/helm.go b/plugins/sops/helm.go index d6cd219a..e783d02c 100644 --- a/plugins/sops/helm.go +++ b/plugins/sops/helm.go @@ -17,15 +17,16 @@ func HelmCLI() schema.Executable { needsauth.NotWithoutArgs(), ), Uses: []schema.CredentialUsage{ - { - Name: credname.SecretKey, - // SOPS age key - provisions SOPS_AGE_KEY env var - }, { Name: sdk.CredentialName("Kubeconfig"), Plugin: "kubernetes", // Kubeconfig - provisions KUBECONFIG env var pointing to temp file }, + { + Name: credname.SecretKey, + Optional: true, + // SOPS age key - provisions SOPS_AGE_KEY env var (optional, only for helm-secrets) + }, }, } } From 5b7de5741e348b946c39aecb10cf974e20aa2b0b Mon Sep 17 00:00:00 2001 From: David Solc Date: Tue, 10 Feb 2026 14:09:42 +0100 Subject: [PATCH 5/8] feat(helm): add standalone helm plugin with kubeconfig + SOPS age key Single credential type combining base64-encoded kubeconfig and optional SOPS age key, avoiding broken cross-plugin credential references. --- plugins/helm/helm.go | 24 ++++++++ plugins/helm/helm_credentials.go | 61 ++++++++++++++++++++ plugins/helm/helm_credentials_test.go | 83 +++++++++++++++++++++++++++ plugins/helm/helmfile.go | 24 ++++++++ plugins/helm/plugin.go | 23 ++++++++ plugins/helm/provisioner.go | 39 +++++++++++++ plugins/helm/test-fixtures/config | 19 ++++++ plugins/plugins.go | 2 + 8 files changed, 275 insertions(+) create mode 100644 plugins/helm/helm.go create mode 100644 plugins/helm/helm_credentials.go create mode 100644 plugins/helm/helm_credentials_test.go create mode 100644 plugins/helm/helmfile.go create mode 100644 plugins/helm/plugin.go create mode 100644 plugins/helm/provisioner.go create mode 100644 plugins/helm/test-fixtures/config diff --git a/plugins/helm/helm.go b/plugins/helm/helm.go new file mode 100644 index 00000000..81a2f59e --- /dev/null +++ b/plugins/helm/helm.go @@ -0,0 +1,24 @@ +package helm + +import ( + "github.com/1Password/shell-plugins/sdk" + "github.com/1Password/shell-plugins/sdk/needsauth" + "github.com/1Password/shell-plugins/sdk/schema" +) + +func HelmCLI() schema.Executable { + return schema.Executable{ + Name: "Helm", + Runs: []string{"helm"}, + DocsURL: sdk.URL("https://helm.sh/docs/"), + NeedsAuth: needsauth.IfAll( + needsauth.NotForHelpOrVersion(), + needsauth.NotWithoutArgs(), + ), + Uses: []schema.CredentialUsage{ + { + Name: sdk.CredentialName("Helm Credentials"), + }, + }, + } +} diff --git a/plugins/helm/helm_credentials.go b/plugins/helm/helm_credentials.go new file mode 100644 index 00000000..1d2240b2 --- /dev/null +++ b/plugins/helm/helm_credentials.go @@ -0,0 +1,61 @@ +package helm + +import ( + "context" + "encoding/base64" + + "github.com/1Password/shell-plugins/sdk" + "github.com/1Password/shell-plugins/sdk/importer" + "github.com/1Password/shell-plugins/sdk/schema" + "github.com/1Password/shell-plugins/sdk/schema/fieldname" +) + +func HelmCredentials() schema.CredentialType { + return schema.CredentialType{ + Name: sdk.CredentialName("Helm Credentials"), + DocsURL: sdk.URL("https://helm.sh/docs/"), + Fields: []schema.CredentialField{ + { + Name: fieldname.Credential, + MarkdownDescription: "Base64-encoded kubeconfig YAML file contents.", + Secret: true, + }, + { + Name: fieldname.PrivateKey, + MarkdownDescription: "Age secret key used by SOPS for encryption and decryption.", + Secret: true, + Optional: true, + Composition: &schema.ValueComposition{ + Prefix: "AGE-SECRET-KEY-", + Charset: schema.Charset{ + Uppercase: true, + Digits: true, + }, + }, + }, + }, + DefaultProvisioner: &helmCredentialsProvisioner{}, + Importer: importer.TryAll( + TryKubeconfigFile(), + TryAgeKeyEnvVar(), + ), + } +} + +func TryKubeconfigFile() sdk.Importer { + return importer.TryFile("~/.kube/config", func(ctx context.Context, contents importer.FileContents, in sdk.ImportInput, out *sdk.ImportAttempt) { + encoded := base64.StdEncoding.EncodeToString(contents) + out.AddCandidate(sdk.ImportCandidate{ + Fields: map[sdk.FieldName]string{ + fieldname.Credential: encoded, + }, + NameHint: importer.SanitizeNameHint("default"), + }) + }) +} + +func TryAgeKeyEnvVar() sdk.Importer { + return importer.TryEnvVarPair(map[string]sdk.FieldName{ + "SOPS_AGE_KEY": fieldname.PrivateKey, + }) +} diff --git a/plugins/helm/helm_credentials_test.go b/plugins/helm/helm_credentials_test.go new file mode 100644 index 00000000..9f0a3df6 --- /dev/null +++ b/plugins/helm/helm_credentials_test.go @@ -0,0 +1,83 @@ +package helm + +import ( + "encoding/base64" + "testing" + + "github.com/1Password/shell-plugins/sdk" + "github.com/1Password/shell-plugins/sdk/plugintest" + "github.com/1Password/shell-plugins/sdk/schema/fieldname" +) + +func TestHelmCredentialsProvisioner(t *testing.T) { + rawConfig := plugintest.LoadFixture(t, "config") + encodedConfig := base64.StdEncoding.EncodeToString([]byte(rawConfig)) + + plugintest.TestProvisioner(t, HelmCredentials().DefaultProvisioner, map[string]plugintest.ProvisionCase{ + "kubeconfig only": { + ItemFields: map[sdk.FieldName]string{ + fieldname.Credential: encodedConfig, + }, + ExpectedOutput: sdk.ProvisionOutput{ + Environment: map[string]string{ + "KUBECONFIG": "/tmp/config", + }, + Files: map[string]sdk.OutputFile{ + "/tmp/config": { + Contents: []byte(rawConfig), + }, + }, + }, + }, + "kubeconfig and sops age key": { + ItemFields: map[sdk.FieldName]string{ + fieldname.Credential: encodedConfig, + fieldname.PrivateKey: "AGE-SECRET-KEY-1QFWENTHXAAPACFPMXHQCREP64GJE5YTHXLX0RPFSXRSPDJGCR0SSWYNX3D", + }, + ExpectedOutput: sdk.ProvisionOutput{ + Environment: map[string]string{ + "KUBECONFIG": "/tmp/config", + "SOPS_AGE_KEY": "AGE-SECRET-KEY-1QFWENTHXAAPACFPMXHQCREP64GJE5YTHXLX0RPFSXRSPDJGCR0SSWYNX3D", + }, + Files: map[string]sdk.OutputFile{ + "/tmp/config": { + Contents: []byte(rawConfig), + }, + }, + }, + }, + }) +} + +func TestHelmCredentialsImporter(t *testing.T) { + rawConfig := plugintest.LoadFixture(t, "config") + encodedConfig := base64.StdEncoding.EncodeToString([]byte(rawConfig)) + + plugintest.TestImporter(t, HelmCredentials().Importer, map[string]plugintest.ImportCase{ + "kubeconfig file": { + Files: map[string]string{ + "~/.kube/config": rawConfig, + }, + ExpectedCandidates: []sdk.ImportCandidate{ + { + Fields: map[sdk.FieldName]string{ + fieldname.Credential: encodedConfig, + }, + NameHint: "", + }, + }, + }, + "age key environment": { + Environment: map[string]string{ + "SOPS_AGE_KEY": "AGE-SECRET-KEY-1QFWENTHXAAPACFPMXHQCREP64GJE5YTHXLX0RPFSXRSPDJGCR0SSWYNX3D", + }, + ExpectedCandidates: []sdk.ImportCandidate{ + { + Fields: map[sdk.FieldName]string{ + fieldname.PrivateKey: "AGE-SECRET-KEY-1QFWENTHXAAPACFPMXHQCREP64GJE5YTHXLX0RPFSXRSPDJGCR0SSWYNX3D", + }, + }, + }, + }, + }) +} diff --git a/plugins/helm/helmfile.go b/plugins/helm/helmfile.go new file mode 100644 index 00000000..28bb2f7a --- /dev/null +++ b/plugins/helm/helmfile.go @@ -0,0 +1,24 @@ +package helm + +import ( + "github.com/1Password/shell-plugins/sdk" + "github.com/1Password/shell-plugins/sdk/needsauth" + "github.com/1Password/shell-plugins/sdk/schema" +) + +func HelmfileCLI() schema.Executable { + return schema.Executable{ + Name: "Helmfile", + Runs: []string{"helmfile"}, + DocsURL: sdk.URL("https://github.com/helmfile/helmfile"), + NeedsAuth: needsauth.IfAll( + needsauth.NotForHelpOrVersion(), + needsauth.NotWithoutArgs(), + ), + Uses: []schema.CredentialUsage{ + { + Name: sdk.CredentialName("Helm Credentials"), + }, + }, + } +} diff --git a/plugins/helm/plugin.go b/plugins/helm/plugin.go new file mode 100644 index 00000000..d53fb406 --- /dev/null +++ b/plugins/helm/plugin.go @@ -0,0 +1,23 @@ +package helm + +import ( + "github.com/1Password/shell-plugins/sdk" + "github.com/1Password/shell-plugins/sdk/schema" +) + +func New() schema.Plugin { + return schema.Plugin{ + Name: "helm", + Platform: schema.PlatformInfo{ + Name: "Helm", + Homepage: sdk.URL("https://helm.sh"), + }, + Credentials: []schema.CredentialType{ + HelmCredentials(), + }, + Executables: []schema.Executable{ + HelmCLI(), + HelmfileCLI(), + }, + } +} diff --git a/plugins/helm/provisioner.go b/plugins/helm/provisioner.go new file mode 100644 index 00000000..b66abec0 --- /dev/null +++ b/plugins/helm/provisioner.go @@ -0,0 +1,39 @@ +package helm + +import ( + "context" + "encoding/base64" + "path/filepath" + + "github.com/1Password/shell-plugins/sdk" + "github.com/1Password/shell-plugins/sdk/schema/fieldname" +) + +type helmCredentialsProvisioner struct{} + +func (p *helmCredentialsProvisioner) Description() string { + return "Provision kubeconfig file and optional SOPS age key for Helm" +} + +func (p *helmCredentialsProvisioner) Provision(ctx context.Context, in sdk.ProvisionInput, out *sdk.ProvisionOutput) { + // Decode base64 kubeconfig and write to temp file + encoded := in.ItemFields[fieldname.Credential] + decoded, err := base64.StdEncoding.DecodeString(encoded) + if err != nil { + out.AddError(err) + return + } + + configPath := filepath.Join(in.TempDir, "config") + out.AddSecretFile(configPath, decoded) + out.AddEnvVar("KUBECONFIG", configPath) + + // Optionally provision SOPS age key + if ageKey, ok := in.ItemFields[fieldname.PrivateKey]; ok && ageKey != "" { + out.AddEnvVar("SOPS_AGE_KEY", ageKey) + } +} + +func (p *helmCredentialsProvisioner) Deprovision(ctx context.Context, in sdk.DeprovisionInput, out *sdk.DeprovisionOutput) { + // Temp files are automatically cleaned up +} diff --git a/plugins/helm/test-fixtures/config b/plugins/helm/test-fixtures/config new file mode 100644 index 00000000..10f840aa --- /dev/null +++ b/plugins/helm/test-fixtures/config @@ -0,0 +1,19 @@ +apiVersion: v1 +kind: Config +clusters: +- cluster: + certificate-authority-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUJlRENDQVIyZ0F3SUJBZ0lCQURBS0JnZ3Foa2pPUFFRREFqQWpNU0V3SHdZRFZRUUREQmhyTTNNdGMyVnkKZG1WeUxXTmhRREUzTURBeU16TTJOREV3SGhjTk1qTXhNVEU1TVRBeU56SXhXaGNOTXpNeE1URTJNVEF5TnpJeApXakFqTVNFd0h3WURWUVFEREJock0zTXRjMlZ5ZG1WeUxXTmhRREUzTURBeU16TTJOREV3V1RBVEJnY3Foa2pPClBRSUJCZ2dxaGtqT1BRTUJCd05DQUFUMUZiY29ycjhBQ1NkcmNqQk5xQ3JSa0M2Y2tMYmZUQ0NSSUN2UndhN2IKUjZiNUxRZHZuOXZVYkhiVHN4Z3FJQ2pYYjZRTEtIRTRrQXlQQ3BDNnBPUEpvMEl3UURBT0JnTlZIUThCQWY4RQpCQU1DQXFRd0R3WURWUjBUQVFIL0JBVXdBd0VCL3pBZEJnTlZIUTRFRmdRVWVlUllndjZBbGFCNVc0dDV0Rm9ZCm9OSHVkVDB3Q2dZSUtvWkl6ajBFQXdJRFNRQXdSZ0loQUk0TlQyR3hxNHYwZExHRlFkNkdtbHdaZ0dBckVZRWoKL1FkeWZsOU12QTZyQWlFQWljVjdiVzlqcVJwNjh4cHdJeDlYak9OeUt2V3dZMVdQVWYzQU1kQTd6T2s9Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K + server: https://10.0.0.1:6443 + name: default +contexts: +- context: + cluster: default + user: default + name: default +current-context: default +preferences: {} +users: +- name: default + user: + client-certificate-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUJrVENDQVRlZ0F3SUJBZ0lJVHVNdTNtNE8waTh3Q2dZSUtvWkl6ajBFQXdJd0l6RWhNQjhHQTFVRUF3d1kKYXpOekxXTnNhV1Z1ZEMxallVQXhOekF3TWpNek5qUXhNQjRYRFRJek1URXhPVEV3TWpjeU1Wb1hEVEkwTVRFeApPREV3TWpjeU1Wb3dNREVYTUJVR0ExVUVDaE1PYzNsemRHVnRPbTFoYzNSbGNuTXhGVEFUQmdOVkJBTVRESE41CmMzUmxiVHBoWkcxcGJqQlpNQk1HQnlxR1NNNDlBZ0VHQ0NxR1NNNDlBd0VIQTBJQUJNL0hvRHZUZmg2dEtoVVUKM3lHZHBrU05EWVhGWjBYYjVVMHRKRzh4TlFiQmEzaDhRQjJMMVN0MDVNRlQ2dEhxUWpsUkJ6cG1qRnJ2L1haQQp1RUl4WmRlalNEQkdNQTRHQTFVZER3RUIvd1FFQXdJRm9EQVRCZ05WSFNVRUREQUtCZ2dyQmdFRkJRY0RBakFmCkJnTlZIU01FR0RBV2dCUmYwYnh0NTVSdlE4M3pxMGxrdnRXU28zYUNDREFLQmdncWhrak9QUVFEQWdOSUFEQkYKQWlFQXo1K2txTWJpMjJKMW1LNGFldlVYUVFGVm9hSXFWRFBzTjZHcW1iZGxaRWtDSUJqeWVxc0tkL0VxdFBXbgpaSXRCL21STC9WdDRHRUhJbGpUckxLNytlN2ZOCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0KLS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUJkekNDQVIyZ0F3SUJBZ0lCQURBS0JnZ3Foa2pPUFFRREFqQWpNU0V3SHdZRFZRUUREQmhyTTNNdFkyeHAKWlc1MExXTmhRREUzTURBeU16TTJOREV3SGhjTk1qTXhNVEU1TVRBeU56SXhXaGNOTXpNeE1URTJNVEF5TnpJeApXakFqTVNFd0h3WURWUVFEREJock0zTXRZMnhwWlc1MExXTmhRREUzTURBeU16TTJOREV3V1RBVEJnY3Foa2pPClBRSUJCZ2dxaGtqT1BRTUJCd05DQUFSMU9ucCsxUnpLYVVVN0lJNTVhTXZJRUFXeE9udmNqd0E0NnRZSDQ3amkKb0dIVFViTHJXc3ZxMHFZeEpJZlkzRmdZekZYMVlkLzhBbVJxWGJrTldkUW9vMEl3UURBT0JnTlZIUThCQWY4RQpCQU1DQXFRd0R3WURWUjBUQVFIL0JBVXdBd0VCL3pBZEJnTlZIUTRFRmdRVVg5RzhiZWVVYjBQTjg2dEpaTC9WCmtxTjJnZ2d3Q2dZSUtvWkl6ajBFQXdJRFNBQXdSUUloQUxoSER3UlllVXBoS0ZSOURKZ2EycjlLRlBmQ2dLSnUKd0JOK2ptOW4wdm1YQWlBenM5Q0hDN1F5aElFSnNmVjVLaGF1MHhqQ1pYdEJpblQ5aEt4d0RGMTJGUT09Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K + client-key-data: LS0tLS1CRUdJTiBFQyBQUklWQVRFIEtFWS0tLS0tCk1IUUNBUUVFSUFTbE5FV3lPRWRuSWxYVWpRdG1Pc1cwWjdwYWxpTUlKdENMcmhKSGZRYlBvQWNHQlN1QkJBQUsKb1VRRFFnQUV6OGVnTzlOK0hxMHFGUlRmSVoybVJJME5oY1ZuUmR2bFRTMGtieksxQnNGcmVIeEFIWXZWSzNUawp3VlBxMGVwQ09WRUhPbWFNV3UvOWRrQzRRakZsMXc9PQotLS0tLUVORCBFQyBQUklWQVRFIEtFWS0tLS0tCg== diff --git a/plugins/plugins.go b/plugins/plugins.go index f7290a48..07573ba7 100644 --- a/plugins/plugins.go +++ b/plugins/plugins.go @@ -25,6 +25,7 @@ import ( "github.com/1Password/shell-plugins/plugins/github" "github.com/1Password/shell-plugins/plugins/gitlab" "github.com/1Password/shell-plugins/plugins/hcloud" + "github.com/1Password/shell-plugins/plugins/helm" "github.com/1Password/shell-plugins/plugins/heroku" "github.com/1Password/shell-plugins/plugins/homebrew" "github.com/1Password/shell-plugins/plugins/huggingface" @@ -91,6 +92,7 @@ func init() { Register(github.New()) Register(gitlab.New()) Register(hcloud.New()) + Register(helm.New()) Register(heroku.New()) Register(homebrew.New()) Register(huggingface.New()) From 4e017b7acefba283dc30191f40fb7e52f6fc1c4c Mon Sep 17 00:00:00 2001 From: David Solc Date: Tue, 10 Feb 2026 14:33:21 +0100 Subject: [PATCH 6/8] refactor(sops): remove helm/helmfile executables Standalone helm plugin now handles these. Having both sops and helm plugins register the same executables causes op to hang. --- plugins/sops/plugin.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/plugins/sops/plugin.go b/plugins/sops/plugin.go index c2613598..f4046cb3 100644 --- a/plugins/sops/plugin.go +++ b/plugins/sops/plugin.go @@ -17,8 +17,6 @@ func New() schema.Plugin { }, Executables: []schema.Executable{ SOPSCLI(), - HelmCLI(), - HelmfileCLI(), }, } } From e3510a2d55841057cd553a566dee03fa7b1216e5 Mon Sep 17 00:00:00 2001 From: David Solc Date: Tue, 10 Feb 2026 16:19:36 +0100 Subject: [PATCH 7/8] fix(helm): write kubeconfig as real file instead of FIFO op CLI creates named pipes (FIFOs) for AddSecretFile outputs. Helm reads the kubeconfig multiple times, blocking on the second FIFO read. Write the file directly with os.WriteFile and clean up in Deprovision. --- plugins/helm/helm_credentials_test.go | 10 ---------- plugins/helm/provisioner.go | 14 +++++++++++--- 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/plugins/helm/helm_credentials_test.go b/plugins/helm/helm_credentials_test.go index 9f0a3df6..c8cf5692 100644 --- a/plugins/helm/helm_credentials_test.go +++ b/plugins/helm/helm_credentials_test.go @@ -22,11 +22,6 @@ func TestHelmCredentialsProvisioner(t *testing.T) { Environment: map[string]string{ "KUBECONFIG": "/tmp/config", }, - Files: map[string]sdk.OutputFile{ - "/tmp/config": { - Contents: []byte(rawConfig), - }, - }, }, }, "kubeconfig and sops age key": { @@ -39,11 +34,6 @@ func TestHelmCredentialsProvisioner(t *testing.T) { "KUBECONFIG": "/tmp/config", "SOPS_AGE_KEY": "AGE-SECRET-KEY-1QFWENTHXAAPACFPMXHQCREP64GJE5YTHXLX0RPFSXRSPDJGCR0SSWYNX3D", }, - Files: map[string]sdk.OutputFile{ - "/tmp/config": { - Contents: []byte(rawConfig), - }, - }, }, }, }) diff --git a/plugins/helm/provisioner.go b/plugins/helm/provisioner.go index b66abec0..1e9d0fb6 100644 --- a/plugins/helm/provisioner.go +++ b/plugins/helm/provisioner.go @@ -3,6 +3,7 @@ package helm import ( "context" "encoding/base64" + "os" "path/filepath" "github.com/1Password/shell-plugins/sdk" @@ -16,7 +17,7 @@ func (p *helmCredentialsProvisioner) Description() string { } func (p *helmCredentialsProvisioner) Provision(ctx context.Context, in sdk.ProvisionInput, out *sdk.ProvisionOutput) { - // Decode base64 kubeconfig and write to temp file + // Decode base64 kubeconfig encoded := in.ItemFields[fieldname.Credential] decoded, err := base64.StdEncoding.DecodeString(encoded) if err != nil { @@ -24,8 +25,13 @@ func (p *helmCredentialsProvisioner) Provision(ctx context.Context, in sdk.Provi return } + // Write kubeconfig as a real file (not via out.AddSecretFile which creates a FIFO). + // Helm reads the kubeconfig multiple times, and FIFOs block on the second read. configPath := filepath.Join(in.TempDir, "config") - out.AddSecretFile(configPath, decoded) + if err := os.WriteFile(configPath, decoded, 0600); err != nil { + out.AddError(err) + return + } out.AddEnvVar("KUBECONFIG", configPath) // Optionally provision SOPS age key @@ -35,5 +41,7 @@ func (p *helmCredentialsProvisioner) Provision(ctx context.Context, in sdk.Provi } func (p *helmCredentialsProvisioner) Deprovision(ctx context.Context, in sdk.DeprovisionInput, out *sdk.DeprovisionOutput) { - // Temp files are automatically cleaned up + // Remove kubeconfig written directly to disk + configPath := filepath.Join(in.TempDir, "config") + os.Remove(configPath) } From 0c10d5d285f211daa0e4caa546d37be4d58a8ef4 Mon Sep 17 00:00:00 2001 From: David Solc Date: Tue, 10 Feb 2026 20:49:35 +0100 Subject: [PATCH 8/8] feat(helm): use cross-plugin ref to sops for age key Simplify helm plugin to kubeconfig-only credential and cross-reference the sops plugin for the optional SOPS age key instead of duplicating it. --- plugins/helm/helm.go | 6 ++--- plugins/helm/helm_credentials.go | 26 +++----------------- plugins/helm/helm_credentials_test.go | 34 ++++----------------------- plugins/helm/helmfile.go | 6 ++--- plugins/helm/plugin.go | 2 +- plugins/helm/provisioner.go | 15 ++++-------- 6 files changed, 20 insertions(+), 69 deletions(-) diff --git a/plugins/helm/helm.go b/plugins/helm/helm.go index 81a2f59e..9c799901 100644 --- a/plugins/helm/helm.go +++ b/plugins/helm/helm.go @@ -4,6 +4,7 @@ import ( "github.com/1Password/shell-plugins/sdk" "github.com/1Password/shell-plugins/sdk/needsauth" "github.com/1Password/shell-plugins/sdk/schema" + "github.com/1Password/shell-plugins/sdk/schema/credname" ) func HelmCLI() schema.Executable { @@ -16,9 +17,8 @@ func HelmCLI() schema.Executable { needsauth.NotWithoutArgs(), ), Uses: []schema.CredentialUsage{ - { - Name: sdk.CredentialName("Helm Credentials"), - }, + {Name: sdk.CredentialName("Kubeconfig")}, + {Name: credname.SecretKey, Plugin: "sops", Optional: true}, }, } } diff --git a/plugins/helm/helm_credentials.go b/plugins/helm/helm_credentials.go index 1d2240b2..92093ca1 100644 --- a/plugins/helm/helm_credentials.go +++ b/plugins/helm/helm_credentials.go @@ -10,9 +10,9 @@ import ( "github.com/1Password/shell-plugins/sdk/schema/fieldname" ) -func HelmCredentials() schema.CredentialType { +func Kubeconfig() schema.CredentialType { return schema.CredentialType{ - Name: sdk.CredentialName("Helm Credentials"), + Name: sdk.CredentialName("Kubeconfig"), DocsURL: sdk.URL("https://helm.sh/docs/"), Fields: []schema.CredentialField{ { @@ -20,24 +20,10 @@ func HelmCredentials() schema.CredentialType { MarkdownDescription: "Base64-encoded kubeconfig YAML file contents.", Secret: true, }, - { - Name: fieldname.PrivateKey, - MarkdownDescription: "Age secret key used by SOPS for encryption and decryption.", - Secret: true, - Optional: true, - Composition: &schema.ValueComposition{ - Prefix: "AGE-SECRET-KEY-", - Charset: schema.Charset{ - Uppercase: true, - Digits: true, - }, - }, - }, }, - DefaultProvisioner: &helmCredentialsProvisioner{}, + DefaultProvisioner: &helmKubeconfigProvisioner{}, Importer: importer.TryAll( TryKubeconfigFile(), - TryAgeKeyEnvVar(), ), } } @@ -53,9 +39,3 @@ func TryKubeconfigFile() sdk.Importer { }) }) } - -func TryAgeKeyEnvVar() sdk.Importer { - return importer.TryEnvVarPair(map[string]sdk.FieldName{ - "SOPS_AGE_KEY": fieldname.PrivateKey, - }) -} diff --git a/plugins/helm/helm_credentials_test.go b/plugins/helm/helm_credentials_test.go index c8cf5692..9139b50a 100644 --- a/plugins/helm/helm_credentials_test.go +++ b/plugins/helm/helm_credentials_test.go @@ -9,12 +9,12 @@ import ( "github.com/1Password/shell-plugins/sdk/schema/fieldname" ) -func TestHelmCredentialsProvisioner(t *testing.T) { +func TestKubeconfigProvisioner(t *testing.T) { rawConfig := plugintest.LoadFixture(t, "config") encodedConfig := base64.StdEncoding.EncodeToString([]byte(rawConfig)) - plugintest.TestProvisioner(t, HelmCredentials().DefaultProvisioner, map[string]plugintest.ProvisionCase{ - "kubeconfig only": { + plugintest.TestProvisioner(t, Kubeconfig().DefaultProvisioner, map[string]plugintest.ProvisionCase{ + "kubeconfig": { ItemFields: map[sdk.FieldName]string{ fieldname.Credential: encodedConfig, }, @@ -24,26 +24,14 @@ func TestHelmCredentialsProvisioner(t *testing.T) { }, }, }, - "kubeconfig and sops age key": { - ItemFields: map[sdk.FieldName]string{ - fieldname.Credential: encodedConfig, - fieldname.PrivateKey: "AGE-SECRET-KEY-1QFWENTHXAAPACFPMXHQCREP64GJE5YTHXLX0RPFSXRSPDJGCR0SSWYNX3D", - }, - ExpectedOutput: sdk.ProvisionOutput{ - Environment: map[string]string{ - "KUBECONFIG": "/tmp/config", - "SOPS_AGE_KEY": "AGE-SECRET-KEY-1QFWENTHXAAPACFPMXHQCREP64GJE5YTHXLX0RPFSXRSPDJGCR0SSWYNX3D", - }, - }, - }, }) } -func TestHelmCredentialsImporter(t *testing.T) { +func TestKubeconfigImporter(t *testing.T) { rawConfig := plugintest.LoadFixture(t, "config") encodedConfig := base64.StdEncoding.EncodeToString([]byte(rawConfig)) - plugintest.TestImporter(t, HelmCredentials().Importer, map[string]plugintest.ImportCase{ + plugintest.TestImporter(t, Kubeconfig().Importer, map[string]plugintest.ImportCase{ "kubeconfig file": { Files: map[string]string{ "~/.kube/config": rawConfig, @@ -57,17 +45,5 @@ func TestHelmCredentialsImporter(t *testing.T) { }, }, }, - "age key environment": { - Environment: map[string]string{ - "SOPS_AGE_KEY": "AGE-SECRET-KEY-1QFWENTHXAAPACFPMXHQCREP64GJE5YTHXLX0RPFSXRSPDJGCR0SSWYNX3D", - }, - ExpectedCandidates: []sdk.ImportCandidate{ - { - Fields: map[sdk.FieldName]string{ - fieldname.PrivateKey: "AGE-SECRET-KEY-1QFWENTHXAAPACFPMXHQCREP64GJE5YTHXLX0RPFSXRSPDJGCR0SSWYNX3D", - }, - }, - }, - }, }) } diff --git a/plugins/helm/helmfile.go b/plugins/helm/helmfile.go index 28bb2f7a..7b2f2119 100644 --- a/plugins/helm/helmfile.go +++ b/plugins/helm/helmfile.go @@ -4,6 +4,7 @@ import ( "github.com/1Password/shell-plugins/sdk" "github.com/1Password/shell-plugins/sdk/needsauth" "github.com/1Password/shell-plugins/sdk/schema" + "github.com/1Password/shell-plugins/sdk/schema/credname" ) func HelmfileCLI() schema.Executable { @@ -16,9 +17,8 @@ func HelmfileCLI() schema.Executable { needsauth.NotWithoutArgs(), ), Uses: []schema.CredentialUsage{ - { - Name: sdk.CredentialName("Helm Credentials"), - }, + {Name: sdk.CredentialName("Kubeconfig")}, + {Name: credname.SecretKey, Plugin: "sops", Optional: true}, }, } } diff --git a/plugins/helm/plugin.go b/plugins/helm/plugin.go index d53fb406..d931f18f 100644 --- a/plugins/helm/plugin.go +++ b/plugins/helm/plugin.go @@ -13,7 +13,7 @@ func New() schema.Plugin { Homepage: sdk.URL("https://helm.sh"), }, Credentials: []schema.CredentialType{ - HelmCredentials(), + Kubeconfig(), }, Executables: []schema.Executable{ HelmCLI(), diff --git a/plugins/helm/provisioner.go b/plugins/helm/provisioner.go index 1e9d0fb6..573d5741 100644 --- a/plugins/helm/provisioner.go +++ b/plugins/helm/provisioner.go @@ -10,13 +10,13 @@ import ( "github.com/1Password/shell-plugins/sdk/schema/fieldname" ) -type helmCredentialsProvisioner struct{} +type helmKubeconfigProvisioner struct{} -func (p *helmCredentialsProvisioner) Description() string { - return "Provision kubeconfig file and optional SOPS age key for Helm" +func (p *helmKubeconfigProvisioner) Description() string { + return "Provision kubeconfig file for Helm" } -func (p *helmCredentialsProvisioner) Provision(ctx context.Context, in sdk.ProvisionInput, out *sdk.ProvisionOutput) { +func (p *helmKubeconfigProvisioner) Provision(ctx context.Context, in sdk.ProvisionInput, out *sdk.ProvisionOutput) { // Decode base64 kubeconfig encoded := in.ItemFields[fieldname.Credential] decoded, err := base64.StdEncoding.DecodeString(encoded) @@ -33,14 +33,9 @@ func (p *helmCredentialsProvisioner) Provision(ctx context.Context, in sdk.Provi return } out.AddEnvVar("KUBECONFIG", configPath) - - // Optionally provision SOPS age key - if ageKey, ok := in.ItemFields[fieldname.PrivateKey]; ok && ageKey != "" { - out.AddEnvVar("SOPS_AGE_KEY", ageKey) - } } -func (p *helmCredentialsProvisioner) Deprovision(ctx context.Context, in sdk.DeprovisionInput, out *sdk.DeprovisionOutput) { +func (p *helmKubeconfigProvisioner) Deprovision(ctx context.Context, in sdk.DeprovisionInput, out *sdk.DeprovisionOutput) { // Remove kubeconfig written directly to disk configPath := filepath.Join(in.TempDir, "config") os.Remove(configPath)