Skip to content

Commit b0ddade

Browse files
authored
convert secret key value to lowercase (#114)
* convert key value to lowercase Signed-off-by: Yingrong Zhao <[email protected]> * add integration test Signed-off-by: Yingrong Zhao <[email protected]> * use porter test bundle Signed-off-by: Yingrong Zhao <[email protected]> * sanitize key string Signed-off-by: Yingrong Zhao <[email protected]> * add unit test Signed-off-by: Yingrong Zhao <[email protected]>
1 parent 3401f52 commit b0ddade

File tree

9 files changed

+258
-23
lines changed

9 files changed

+258
-23
lines changed

magefile.go

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -196,11 +196,8 @@ func testLocalIntegration() {
196196

197197
// Run integration tests against the test cluster.
198198
func TestIntegration() {
199-
mg.Deps(XBuildAll, EnsureGinkgo, CleanTestdata)
199+
mg.Deps(CleanTestdata, XBuildAll, EnsureGinkgo)
200200
mg.Deps(EnsureTestNamespace)
201-
defer func() {
202-
CleanTestdata()
203-
}()
204201

205202
if os.Getenv("PORTER_AGENT_REPOSITORY") != "" && os.Getenv("PORTER_AGENT_VERSION") != "" {
206203
localAgentImgRepository = os.Getenv("PORTER_AGENT_REPOSITORY")

pkg/kubernetes/secrets/store.go

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package secrets
33
import (
44
"context"
55
"fmt"
6+
"regexp"
67
"strings"
78

89
k8shelper "get.porter.sh/plugin/kubernetes/pkg/kubernetes/helper"
@@ -74,7 +75,7 @@ func (s *Store) Resolve(ctx context.Context, keyName string, keyValue string) (s
7475
return s.hostStore.Resolve(keyName, keyValue)
7576
}
7677
s.logger.Debug(fmt.Sprintf("Store.Resolve: ns:%s, keyName:%s, keyValue:%s", s.namespace, keyName, keyValue))
77-
key := strings.ToLower(keyValue)
78+
key := SanitizeKey(keyValue)
7879

7980
secret, err := s.clientSet.CoreV1().Secrets(s.namespace).Get(ctx, key, metav1.GetOptions{})
8081
if err != nil {
@@ -98,8 +99,9 @@ func (s *Store) Create(ctx context.Context, keyName string, keyValue string, val
9899
return err
99100
}
100101

101-
key := strings.ToLower(keyName)
102102
s.logger.Debug(fmt.Sprintf("Store.Create: ns:%s, keyName:%s, keyValue:%s", s.namespace, keyName, keyValue))
103+
104+
key := strings.ToLower(keyName)
103105
if key != SecretSourceType {
104106
return log.Error(fmt.Errorf("unsupported secret type: %s. Only %s is supported", keyName, SecretSourceType))
105107
}
@@ -113,7 +115,20 @@ func (s *Store) Create(ctx context.Context, keyName string, keyValue string, val
113115
data := map[string][]byte{
114116
SecretDataKey: byteValue,
115117
}
116-
secret := &v1.Secret{ObjectMeta: metav1.ObjectMeta{Name: keyValue}, Immutable: &Immutable, Data: data}
118+
secret := &v1.Secret{ObjectMeta: metav1.ObjectMeta{Name: SanitizeKey(keyValue)}, Immutable: &Immutable, Data: data}
117119
_, err := s.clientSet.CoreV1().Secrets(s.namespace).Create(ctx, secret, metav1.CreateOptions{})
118120
return log.Error(err)
119121
}
122+
123+
// SanitizeKey converts a string to follow below rules:
124+
// 1. only contains lower case alphanumeric characters, '-' or '.'
125+
// 2. must start and end with an alphanumeric character
126+
func SanitizeKey(v string) string {
127+
key := strings.ToLower(v)
128+
// replace non-alphanumeric characters at the beginning and the end of the string
129+
startEndReg := regexp.MustCompile(`^[^a-z0-9]|[^a-z0-9]$`)
130+
firstPass := startEndReg.ReplaceAllString(key, "000")
131+
// replace non-alphanumeric characters except `-` and `.` in the string
132+
characterReg := regexp.MustCompile(`[^a-z0-9-.]+`)
133+
return characterReg.ReplaceAllString(firstPass, "-")
134+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package secrets_test
2+
3+
import (
4+
"testing"
5+
6+
"get.porter.sh/plugin/kubernetes/pkg/kubernetes/secrets"
7+
"github.com/stretchr/testify/require"
8+
)
9+
10+
func TestSanitizeKey(t *testing.T) {
11+
tests := []struct {
12+
desc string
13+
input string
14+
expected string
15+
}{
16+
{"with non-alphanumeric character at the start and end of the string", "-hello_", "000hello000"},
17+
{"with invalid symbols in the string", "-he_*llo.", "000he-llo000"},
18+
}
19+
for _, tt := range tests {
20+
tt := tt
21+
t.Run(tt.desc, func(t *testing.T) {
22+
require.Equal(t, tt.expected, secrets.SanitizeKey(tt.input), "failed to sanitize input: %s", tt.input)
23+
})
24+
}
25+
}

tests/integration/local/secrets/store_test.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,15 @@ func TestCreate_Secret(t *testing.T) {
139139
require.Equal(t, "testValue", resolved)
140140
})
141141

142+
t.Run("successfully store a secret that doesn't match the secret key naming requirements", func(t *testing.T) {
143+
err := store.Create(context.Background(), secrets.SecretSourceType, "-UPPERCA_SE-test-", "testValue")
144+
require.NoError(t, err)
145+
146+
resolved, err := store.Resolve(context.Background(), secrets.SecretSourceType, "-UPPERCA_SE-test-")
147+
require.NoError(t, err)
148+
require.Equal(t, "testValue", resolved)
149+
})
150+
142151
t.Run("exceeded maximum secret value size", func(t *testing.T) {
143152
invalidValue := make([]byte, v1.MaxSecretSize+1)
144153
err := store.Create(context.Background(), secrets.SecretSourceType, "testkey-max-secret-size", string(invalidValue))

tests/integration/operator/ginkgo/installation_test.go

Lines changed: 61 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"context"
88
"encoding/json"
99
"fmt"
10+
"strings"
1011
"time"
1112

1213
porterv1 "get.porter.sh/operator/api/v1"
@@ -38,8 +39,8 @@ var _ = Describe("Porter using default secrets plugin config", func() {
3839
installationName := fmt.Sprintf("default-plugin-%v", randId)
3940
ns := createTestNamespace(context.Background())
4041
ctx := context.Background()
41-
createSecret(ns, secrets.SecretDataKey, "password", "test")
42-
credSet := NewCredSet("test", "insecureValue", "password")
42+
createSecret(ns, secrets.SecretDataKey, "cred-password", "test")
43+
credSet := NewCredSet("test", "insecureValue", "cred-password")
4344
agentAction := createCredentialSetAgentAction(ns, credSet)
4445
pollAA := func() bool { return agentActionPoll(agentAction) }
4546
Eventually(pollAA, time.Second*120, time.Second*3).Should(BeTrue())
@@ -64,8 +65,8 @@ var _ = Describe("Porter using default secrets plugin config", func() {
6465
installationName := fmt.Sprintf("default-plugin-%v", randId)
6566
installationNs := createTestNamespace(ctx)
6667
secretsNs := createTestNamespace(ctx)
67-
createSecret(secretsNs, secrets.SecretDataKey, "password", "test")
68-
credSet := NewCredSet("test", "insecureValue", "password")
68+
createSecret(secretsNs, secrets.SecretDataKey, "cred-password", "test")
69+
credSet := NewCredSet("test", "insecureValue", "cred-password")
6970
agentAction := createCredentialSetAgentAction(installationNs, credSet)
7071
pollAA := func() bool { return agentActionPoll(agentAction) }
7172
Eventually(pollAA, time.Second*120, time.Second*3).Should(BeTrue())
@@ -89,8 +90,8 @@ var _ = Describe("Porter using default secrets plugin config", func() {
8990
ctx := context.Background()
9091
installationName := fmt.Sprintf("default-plugin-%v", randId)
9192
installationNs := createTestNamespace(ctx)
92-
createSecret(installationNs, "invalidKey", "password", "test")
93-
credSet := NewCredSet("test", "insecureValue", "password")
93+
createSecret(installationNs, "invalidKey", "cred-password", "test")
94+
credSet := NewCredSet("test", "insecureValue", "cred-password")
9495
agentAction := createCredentialSetAgentAction(installationNs, credSet)
9596
pollAA := func() bool { return agentActionPoll(agentAction) }
9697
Eventually(pollAA, time.Second*120, time.Second*3).Should(BeTrue())
@@ -119,13 +120,13 @@ var _ = Describe("Porter using a secrets plugin config that doesn't specify the
119120
defaultSecretsCfgName := "kubernetes-secrets"
120121
ns := createTestNamespace(context.Background())
121122
ctx := context.Background()
122-
createSecret(ns, secrets.SecretDataKey, "password", "test")
123+
createSecret(ns, secrets.SecretDataKey, "cred-password", "test")
123124
porterCfg := NewPorterConfig(ns)
124125
k8sSecretsCfg := NewSecretsPluginConfig(defaultSecretsCfgName, nil)
125126
SetPorterConfigSecrets(porterCfg, k8sSecretsCfg)
126127
porterCfg.Spec.DefaultSecrets = pointer.String(defaultSecretsCfgName)
127128
Expect(k8sClient.Create(context.Background(), porterCfg)).Should(Succeed())
128-
credSet := NewCredSet("test", "insecureValue", "password")
129+
credSet := NewCredSet("test", "insecureValue", "cred-password")
129130
agentAction := createCredentialSetAgentAction(ns, credSet)
130131
pollAA := func() bool { return agentActionPoll(agentAction) }
131132
Eventually(pollAA, time.Second*120, time.Second*3).Should(BeTrue())
@@ -158,15 +159,15 @@ var _ = Describe("Porter using secrets plugin configured using same namespace as
158159
installationName := fmt.Sprintf("porter-hello-%v", randId)
159160
ns := createTestNamespace(context.Background())
160161
ctx := context.Background()
161-
createSecret(ns, secrets.SecretDataKey, "password", "test")
162+
createSecret(ns, secrets.SecretDataKey, "cred-password", "test")
162163
defaultSecretsCfgName := "kubernetes-secrets"
163164
porterCfg := NewPorterConfig(ns)
164165
secretsNamespaceCfg := &SecretsConfig{Namespace: ns}
165166
k8sSecretsCfg := NewSecretsPluginConfig(defaultSecretsCfgName, secretsNamespaceCfg)
166167
SetPorterConfigSecrets(porterCfg, k8sSecretsCfg)
167168
porterCfg.Spec.DefaultSecrets = pointer.String(defaultSecretsCfgName)
168169
Expect(k8sClient.Create(context.Background(), porterCfg)).Should(Succeed())
169-
credSet := NewCredSet("test", "insecureValue", "password")
170+
credSet := NewCredSet("test", "insecureValue", "cred-password")
170171
agentAction := createCredentialSetAgentAction(ns, credSet)
171172
pollAA := func() bool { return agentActionPoll(agentAction) }
172173
Eventually(pollAA, time.Second*120, time.Second*3).Should(BeTrue())
@@ -184,6 +185,53 @@ var _ = Describe("Porter using secrets plugin configured using same namespace as
184185
validateInstallStatus(inst, porterv1.PhaseSucceeded)
185186
})
186187
})
188+
189+
When("applying an Installation with a sensitive parameter in the Installation namespace", func() {
190+
It("successfully installs", func() {
191+
By("storing secrets in the kubernetes secret")
192+
randId := uuid.New()
193+
installationName := fmt.Sprintf("porter-hello-secret-%v", randId)
194+
ns := createTestNamespace(context.Background())
195+
ctx := context.Background()
196+
createSecret(ns, secrets.SecretDataKey, "cred-test", "test")
197+
defaultSecretsCfgName := "kubernetes-secrets"
198+
porterCfg := NewPorterConfig(ns)
199+
secretsNamespaceCfg := &SecretsConfig{Namespace: ns}
200+
k8sSecretsCfg := NewSecretsPluginConfig(defaultSecretsCfgName, secretsNamespaceCfg)
201+
SetPorterConfigSecrets(porterCfg, k8sSecretsCfg)
202+
porterCfg.Spec.DefaultSecrets = pointer.String(defaultSecretsCfgName)
203+
Expect(k8sClient.Create(context.Background(), porterCfg)).Should(Succeed())
204+
credSet := NewCredSet("test", "insecureValue", "cred-test")
205+
agentAction := createCredentialSetAgentAction(ns, credSet)
206+
pollAA := func() bool { return agentActionPoll(agentAction) }
207+
Eventually(pollAA, time.Second*120, time.Second*3).Should(BeTrue())
208+
inst := NewInstallation(installationName, ns)
209+
inst.Spec.Parameters = runtime.RawExtension{Raw: []byte("{\"delay\": \"1\", \"exitStatus\": \"0\", \"password\": \"super-secret\"}")}
210+
Expect(k8sClient.Create(ctx, inst)).Should(Succeed())
211+
212+
// Wait for the job to be created
213+
installations := waitForInstallationStarted(ctx, ns, installationName)
214+
installation := installations.Items[0]
215+
216+
// Validate that the job succeeded
217+
installation = waitForInstallationFinished(ctx, installation)
218+
219+
// Validate that the installation status was updated
220+
validateInstallStatus(inst, porterv1.PhaseSucceeded)
221+
secretList := &corev1.SecretList{}
222+
Expect(k8sClient.List(ctx, secretList, client.InNamespace(ns))).Should(Succeed())
223+
var found bool
224+
for _, s := range secretList.Items {
225+
if !strings.Contains(s.ObjectMeta.Name, "password") {
226+
continue
227+
}
228+
if value, ok := s.Data[secrets.SecretDataKey]; ok {
229+
found = string(value) == "super-secret"
230+
}
231+
}
232+
Expect(found).Should(BeTrue())
233+
})
234+
})
187235
})
188236

189237
var _ = Describe("Porter k8s secrets plugin configured using a different namespace than the Installation resource", func() {
@@ -195,7 +243,7 @@ var _ = Describe("Porter k8s secrets plugin configured using a different namespa
195243
installNamespace := createTestNamespace(context.Background())
196244
secretNamespace := createTestNamespace(context.Background())
197245
ctx := context.Background()
198-
var defaultSecretsCfgName, secretName, secretValue, credSetName = "kubernetes-secrets", "password", "test", "test"
246+
var defaultSecretsCfgName, secretName, secretValue, credSetName = "kubernetes-secrets", "cred-password", "test", "test"
199247
createSecret(secretNamespace, secrets.SecretDataKey, secretName, secretValue)
200248
porterCfg := NewPorterConfig(installNamespace)
201249
secretsNamespaceCfg := &SecretsConfig{Namespace: secretNamespace}
@@ -256,8 +304,8 @@ func NewInstallation(installationName, installationNamespace string) *porterv1.I
256304
Name: installationName,
257305
Namespace: installationNamespace,
258306
Bundle: porterv1.OCIReferenceParts{
259-
Repository: "ghcr.io/bdegeeter/porter-test-me",
260-
Version: "0.2.0",
307+
Repository: "ghcr.io/getporter/test/kubernetes-plugin",
308+
Version: "0.1.0",
261309
},
262310
Parameters: runtime.RawExtension{Raw: []byte("{\"delay\": \"1\", \"exitStatus\": \"0\"}")},
263311
CredentialSets: []string{"test"},
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
schemaVersion: 1.0.0-alpha.1
21
apiVersion: porter.sh/v1
32
kind: Installation
43
metadata:
@@ -8,10 +7,11 @@ spec:
87
namespace: dev
98
name: foo2
109
bundle:
11-
repository: ghcr.io/bdegeeter/porter-test-me
12-
version: 0.2.0
10+
repository: ghcr.io/getporter/test/kubernetes-plugin
11+
version: 0.1.0
1312
parameters:
1413
delay: 0
1514
exitStatus: 0
15+
password: super-secret
1616
credentialSets:
1717
- test
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
FROM debian:stretch-slim
2+
3+
ARG BUNDLE_DIR
4+
5+
RUN apt-get update && apt-get install -y ca-certificates jq
6+
7+
# This is a template Dockerfile for the bundle's invocation image
8+
# You can customize it to use different base images, install tools and copy configuration files.
9+
#
10+
# Porter will use it as a template and append lines to it for the mixins
11+
# and to set the CMD appropriately for the CNAB specification.
12+
#
13+
# Add the following line to porter.yaml to instruct Porter to use this template
14+
# dockerfile: Dockerfile.tmpl
15+
16+
# You can control where the mixin's Dockerfile lines are inserted into this file by moving "# PORTER_MIXINS" line
17+
# another location in this file. If you remove that line, the mixins generated content is appended to this file.
18+
# PORTER_MIXINS
19+
20+
# Use the BUNDLE_DIR build argument to copy files into the bundle
21+
COPY . $BUNDLE_DIR
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
# This is the configuration for Porter
2+
# You must define steps for each action, but the rest is optional
3+
# See https://porter.sh/author-bundles for documentation on how to configure your bundle
4+
# Uncomment out the sections below to take full advantage of what Porter can do!
5+
6+
schemaVersion: 1.0.0-alpha.1
7+
name: kubernetes-plugin
8+
version: 0.1.0
9+
description: "Porter bundle with test behaviors"
10+
registry: ghcr.io/getporter/test
11+
12+
# If you want to customize the Dockerfile in use, uncomment the line below and update the referenced file.
13+
# See https://porter.sh/custom-dockerfile/
14+
dockerfile: Dockerfile.tmpl
15+
16+
mixins:
17+
- exec
18+
19+
# Below is an example of how to define credentials
20+
# See https://porter.sh/author-bundles/#credentials
21+
credentials:
22+
- name: insecureValue
23+
description: insecure test credential
24+
env: INSECURE_VALUE
25+
required: false
26+
27+
# Below is an example of how to define parameters
28+
# See https://porter.sh/author-bundles/#parameters
29+
parameters:
30+
- name: exitStatus
31+
description: control exit status code
32+
type: integer
33+
default: 0
34+
minimum: 0
35+
36+
- name: delay
37+
description: sleep (in seconds) before exiting
38+
type: integer
39+
default: 0
40+
minimum: 0
41+
42+
- name: password
43+
description: super secret password
44+
type: string
45+
default: default-value
46+
sensitive: true
47+
48+
outputs:
49+
- name: outAction
50+
description: bundle action
51+
type: string
52+
- name: outDelay
53+
description: delay parameter value
54+
type: integer
55+
- name: outExitStatus
56+
description: exitStatus parameter value
57+
type: integer
58+
- name: outInsecureValue
59+
description: insecureValue secret value
60+
type: string
61+
- name: outSecureValue
62+
description: secureValue secret value
63+
type: string
64+
sensitive: true
65+
66+
install:
67+
- exec:
68+
description: "test install"
69+
command: ./test-me.sh
70+
arguments:
71+
- install
72+
- "{{ bundle.parameters.delay }}"
73+
- "{{ bundle.parameters.exitStatus }}"
74+
- "{{ bundle.parameters.password }}"
75+
outputs:
76+
- name: outAction
77+
jsonPath: "$.config.action"
78+
- name: outDelay
79+
jsonPath: "$.config.parameters.delay"
80+
- name: outExitStatus
81+
jsonPath: "$.config.parameters.exitStatus"
82+
- name: outSecureValue
83+
jsonPath: "$.config.parameters.password"
84+
- name: outInsecureValue
85+
jsonPath: "$.credentials.insecureValue"
86+
87+
uninstall:
88+
- exec:
89+
description: "test uninstall"
90+
command: ./test-me.sh
91+
arguments:
92+
- uninstall
93+
- "{{ bundle.parameters.delay }}"
94+
- "{{ bundle.parameters.exitStatus }}"
95+
outputs:
96+
- name: outAction
97+
jsonPath: "$.config.action"
98+
- name: outDelay
99+
jsonPath: "$.config.parameters.delay"
100+
- name: outExitStatus
101+
jsonPath: "$.config.parameters.exitStatus"

0 commit comments

Comments
 (0)