1601 lines
42 KiB
Go
1601 lines
42 KiB
Go
/*
|
|
Copyright 2021 The Flux authors
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
package decryptor
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/base64"
|
|
"fmt"
|
|
"io/fs"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
extage "filippo.io/age"
|
|
"github.com/getsops/sops/v3"
|
|
"github.com/getsops/sops/v3/age"
|
|
"github.com/getsops/sops/v3/cmd/sops/formats"
|
|
. "github.com/onsi/gomega"
|
|
gt "github.com/onsi/gomega/types"
|
|
corev1 "k8s.io/api/core/v1"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
|
"sigs.k8s.io/controller-runtime/pkg/client/fake"
|
|
"sigs.k8s.io/kustomize/api/konfig"
|
|
"sigs.k8s.io/kustomize/api/provider"
|
|
"sigs.k8s.io/kustomize/api/resource"
|
|
kustypes "sigs.k8s.io/kustomize/api/types"
|
|
"sigs.k8s.io/yaml"
|
|
|
|
"github.com/fluxcd/pkg/apis/meta"
|
|
|
|
kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1"
|
|
)
|
|
|
|
func TestIsEncryptedSecret(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
object []byte
|
|
want gt.GomegaMatcher
|
|
}{
|
|
{name: "encrypted secret", object: []byte("apiVersion: v1\nkind: Secret\nsops: true\n"), want: BeTrue()},
|
|
{name: "decrypted secret", object: []byte("apiVersion: v1\nkind: Secret\n"), want: BeFalse()},
|
|
{name: "other resource", object: []byte("apiVersion: v1\nkind: Deployment\n"), want: BeFalse()},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
g := NewWithT(t)
|
|
|
|
u := &unstructured.Unstructured{}
|
|
g.Expect(yaml.Unmarshal(tt.object, u)).To(Succeed())
|
|
g.Expect(IsEncryptedSecret(u)).To(tt.want)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDecryptor_ImportKeys(t *testing.T) {
|
|
g := NewWithT(t)
|
|
|
|
const provider = "sops"
|
|
|
|
pgpKey, err := os.ReadFile("testdata/pgp.asc")
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
ageKey, err := os.ReadFile("testdata/age.txt")
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
|
|
tests := []struct {
|
|
name string
|
|
decryption *kustomizev1.Decryption
|
|
secret *corev1.Secret
|
|
wantErr bool
|
|
inspectFunc func(g *GomegaWithT, decryptor *Decryptor)
|
|
}{
|
|
{
|
|
name: "PGP key",
|
|
decryption: &kustomizev1.Decryption{
|
|
Provider: provider,
|
|
SecretRef: &meta.LocalObjectReference{
|
|
Name: "pgp-secret",
|
|
},
|
|
},
|
|
secret: &corev1.Secret{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "pgp-secret",
|
|
Namespace: provider,
|
|
},
|
|
Data: map[string][]byte{
|
|
"pgp" + DecryptionPGPExt: pgpKey,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "PGP key import error",
|
|
decryption: &kustomizev1.Decryption{
|
|
Provider: provider,
|
|
SecretRef: &meta.LocalObjectReference{
|
|
Name: "pgp-secret",
|
|
},
|
|
},
|
|
secret: &corev1.Secret{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "pgp-secret",
|
|
Namespace: provider,
|
|
},
|
|
Data: map[string][]byte{
|
|
"pgp" + DecryptionPGPExt: []byte("not-a-valid-armored-key"),
|
|
},
|
|
},
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "age key",
|
|
decryption: &kustomizev1.Decryption{
|
|
Provider: provider,
|
|
SecretRef: &meta.LocalObjectReference{
|
|
Name: "age-secret",
|
|
},
|
|
},
|
|
secret: &corev1.Secret{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "age-secret",
|
|
Namespace: provider,
|
|
},
|
|
Data: map[string][]byte{
|
|
"age" + DecryptionAgeExt: ageKey,
|
|
},
|
|
},
|
|
inspectFunc: func(g *GomegaWithT, decryptor *Decryptor) {
|
|
g.Expect(decryptor.ageIdentities).To(HaveLen(1))
|
|
},
|
|
},
|
|
{
|
|
name: "age key import error",
|
|
decryption: &kustomizev1.Decryption{
|
|
Provider: provider,
|
|
SecretRef: &meta.LocalObjectReference{
|
|
Name: "age-secret",
|
|
},
|
|
},
|
|
secret: &corev1.Secret{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "age-secret",
|
|
Namespace: provider,
|
|
},
|
|
Data: map[string][]byte{
|
|
"age" + DecryptionAgeExt: []byte("not-a-valid-key"),
|
|
},
|
|
},
|
|
wantErr: true,
|
|
inspectFunc: func(g *GomegaWithT, decryptor *Decryptor) {
|
|
g.Expect(decryptor.ageIdentities).To(HaveLen(0))
|
|
},
|
|
},
|
|
{
|
|
name: "HC Vault token",
|
|
decryption: &kustomizev1.Decryption{
|
|
Provider: provider,
|
|
SecretRef: &meta.LocalObjectReference{
|
|
Name: "hcvault-secret",
|
|
},
|
|
},
|
|
secret: &corev1.Secret{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "hcvault-secret",
|
|
Namespace: provider,
|
|
},
|
|
Data: map[string][]byte{
|
|
DecryptionVaultTokenFileName: []byte("some-hcvault-token"),
|
|
},
|
|
},
|
|
inspectFunc: func(g *GomegaWithT, decryptor *Decryptor) {
|
|
g.Expect(decryptor.vaultToken).To(Equal("some-hcvault-token"))
|
|
},
|
|
},
|
|
{
|
|
name: "AWS KMS credentials",
|
|
decryption: &kustomizev1.Decryption{
|
|
Provider: provider,
|
|
SecretRef: &meta.LocalObjectReference{
|
|
Name: "awskms-secret",
|
|
},
|
|
},
|
|
secret: &corev1.Secret{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "awskms-secret",
|
|
Namespace: provider,
|
|
},
|
|
Data: map[string][]byte{
|
|
DecryptionAWSKmsFile: []byte(`aws_access_key_id: test-id
|
|
aws_secret_access_key: test-secret
|
|
aws_session_token: test-token`),
|
|
},
|
|
},
|
|
inspectFunc: func(g *GomegaWithT, decryptor *Decryptor) {
|
|
g.Expect(decryptor.awsCredsProvider).ToNot(BeNil())
|
|
},
|
|
},
|
|
{
|
|
name: "GCP Service Account key",
|
|
decryption: &kustomizev1.Decryption{
|
|
Provider: provider,
|
|
SecretRef: &meta.LocalObjectReference{
|
|
Name: "gcpkms-secret",
|
|
},
|
|
},
|
|
secret: &corev1.Secret{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "gcpkms-secret",
|
|
Namespace: provider,
|
|
},
|
|
Data: map[string][]byte{
|
|
DecryptionGCPCredsFile: []byte(`{ "client_id": "<client-id>.apps.googleusercontent.com",
|
|
"client_secret": "<secret>",
|
|
"type": "authorized_user"}`),
|
|
},
|
|
},
|
|
inspectFunc: func(g *GomegaWithT, decryptor *Decryptor) {
|
|
g.Expect(decryptor.gcpCredsJSON).ToNot(BeNil())
|
|
},
|
|
},
|
|
{
|
|
name: "Azure Key Vault token",
|
|
decryption: &kustomizev1.Decryption{
|
|
Provider: provider,
|
|
SecretRef: &meta.LocalObjectReference{
|
|
Name: "azkv-secret",
|
|
},
|
|
},
|
|
secret: &corev1.Secret{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "azkv-secret",
|
|
Namespace: provider,
|
|
},
|
|
Data: map[string][]byte{
|
|
DecryptionAzureAuthFile: []byte(`tenantId: some-tenant-id
|
|
clientId: some-client-id
|
|
clientSecret: some-client-secret`),
|
|
},
|
|
},
|
|
inspectFunc: func(g *GomegaWithT, decryptor *Decryptor) {
|
|
g.Expect(decryptor.azureToken).ToNot(BeNil())
|
|
},
|
|
},
|
|
{
|
|
name: "Azure Key Vault token load config error",
|
|
decryption: &kustomizev1.Decryption{
|
|
Provider: provider,
|
|
SecretRef: &meta.LocalObjectReference{
|
|
Name: "azkv-secret",
|
|
},
|
|
},
|
|
secret: &corev1.Secret{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "azkv-secret",
|
|
Namespace: provider,
|
|
},
|
|
Data: map[string][]byte{
|
|
DecryptionAzureAuthFile: []byte(`{"malformed\: JSON"}`),
|
|
},
|
|
},
|
|
wantErr: true,
|
|
inspectFunc: func(g *GomegaWithT, decryptor *Decryptor) {
|
|
g.Expect(decryptor.azureToken).To(BeNil())
|
|
},
|
|
},
|
|
{
|
|
name: "Azure Key Vault unsupported config",
|
|
decryption: &kustomizev1.Decryption{
|
|
Provider: provider,
|
|
SecretRef: &meta.LocalObjectReference{
|
|
Name: "azkv-secret",
|
|
},
|
|
},
|
|
secret: &corev1.Secret{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "azkv-secret",
|
|
Namespace: provider,
|
|
},
|
|
Data: map[string][]byte{
|
|
DecryptionAzureAuthFile: []byte(`tenantId: incomplete`),
|
|
},
|
|
},
|
|
wantErr: true,
|
|
inspectFunc: func(g *GomegaWithT, decryptor *Decryptor) {
|
|
g.Expect(decryptor.azureToken).To(BeNil())
|
|
},
|
|
},
|
|
{
|
|
name: "multiple Secret data entries",
|
|
decryption: &kustomizev1.Decryption{
|
|
Provider: provider,
|
|
SecretRef: &meta.LocalObjectReference{
|
|
Name: "multiple-secret",
|
|
},
|
|
},
|
|
secret: &corev1.Secret{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "multiple-secret",
|
|
Namespace: provider,
|
|
},
|
|
Data: map[string][]byte{
|
|
"age" + DecryptionAgeExt: ageKey,
|
|
DecryptionVaultTokenFileName: []byte("some-hcvault-token"),
|
|
},
|
|
},
|
|
inspectFunc: func(g *GomegaWithT, decryptor *Decryptor) {
|
|
g.Expect(decryptor.vaultToken).ToNot(BeEmpty())
|
|
g.Expect(decryptor.ageIdentities).To(HaveLen(1))
|
|
},
|
|
},
|
|
{
|
|
name: "no Decryption spec",
|
|
decryption: nil,
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "no Decryption Secret",
|
|
decryption: &kustomizev1.Decryption{
|
|
Provider: DecryptionProviderSOPS,
|
|
},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "non-existing Decryption Secret",
|
|
decryption: &kustomizev1.Decryption{
|
|
Provider: DecryptionProviderSOPS,
|
|
SecretRef: &meta.LocalObjectReference{
|
|
Name: "does-not-exist",
|
|
},
|
|
},
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "unimplemented Decryption Provider",
|
|
decryption: &kustomizev1.Decryption{
|
|
Provider: "not-supported",
|
|
},
|
|
wantErr: false,
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
g := NewWithT(t)
|
|
|
|
cb := fake.NewClientBuilder()
|
|
if tt.secret != nil {
|
|
cb.WithObjects(tt.secret)
|
|
}
|
|
kustomization := kustomizev1.Kustomization{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: provider + "-" + tt.name,
|
|
Namespace: provider,
|
|
},
|
|
Spec: kustomizev1.KustomizationSpec{
|
|
Interval: metav1.Duration{Duration: 2 * time.Minute},
|
|
Path: "./",
|
|
Decryption: tt.decryption,
|
|
},
|
|
}
|
|
|
|
d, cleanup, err := NewTempDecryptor("", cb.Build(), &kustomization)
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
t.Cleanup(cleanup)
|
|
|
|
match := Succeed()
|
|
if tt.wantErr {
|
|
match = HaveOccurred()
|
|
}
|
|
g.Expect(d.ImportKeys(context.TODO())).To(match)
|
|
|
|
if tt.inspectFunc != nil {
|
|
tt.inspectFunc(g, d)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDecryptor_SopsDecryptWithFormat(t *testing.T) {
|
|
t.Run("decrypt INI to INI", func(t *testing.T) {
|
|
g := NewWithT(t)
|
|
|
|
ageID, err := extage.GenerateX25519Identity()
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
|
|
kd := &Decryptor{
|
|
checkSopsMac: true,
|
|
ageIdentities: age.ParsedIdentities{ageID},
|
|
}
|
|
|
|
format := formats.Ini
|
|
data := []byte("[config]\nkey = value\n")
|
|
encData, err := kd.sopsEncryptWithFormat(sops.Metadata{
|
|
KeyGroups: []sops.KeyGroup{
|
|
{&age.MasterKey{Recipient: ageID.Recipient().String()}},
|
|
},
|
|
}, data, format, format)
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
g.Expect(bytes.Contains(encData, sopsFormatToMarkerBytes[format])).To(BeTrue())
|
|
g.Expect(encData).ToNot(Equal(data))
|
|
|
|
out, err := kd.SopsDecryptWithFormat(encData, format, format)
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
g.Expect(out).To(Equal(data))
|
|
})
|
|
|
|
t.Run("decrypt JSON to YAML", func(t *testing.T) {
|
|
g := NewWithT(t)
|
|
|
|
ageID, err := extage.GenerateX25519Identity()
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
|
|
kd := &Decryptor{
|
|
checkSopsMac: true,
|
|
ageIdentities: age.ParsedIdentities{ageID},
|
|
}
|
|
|
|
inputFormat, outputFormat := formats.Json, formats.Yaml
|
|
data := []byte("{\"key\": \"value\"}\n")
|
|
encData, err := kd.sopsEncryptWithFormat(sops.Metadata{
|
|
KeyGroups: []sops.KeyGroup{
|
|
{&age.MasterKey{Recipient: ageID.Recipient().String()}},
|
|
},
|
|
}, data, inputFormat, inputFormat)
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
g.Expect(bytes.Contains(encData, sopsFormatToMarkerBytes[inputFormat])).To(BeTrue())
|
|
|
|
out, err := kd.SopsDecryptWithFormat(encData, inputFormat, outputFormat)
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
g.Expect(out).To(Equal([]byte("key: value\n")))
|
|
})
|
|
|
|
t.Run("invalid JSON data", func(t *testing.T) {
|
|
g := NewWithT(t)
|
|
|
|
format := formats.Json
|
|
data, err := (&Decryptor{}).SopsDecryptWithFormat([]byte("invalid json"), format, format)
|
|
g.Expect(err).To(HaveOccurred())
|
|
g.Expect(err.Error()).To(ContainSubstring("failed to load encrypted JSON data"))
|
|
g.Expect(data).To(BeNil())
|
|
})
|
|
|
|
t.Run("no data key", func(t *testing.T) {
|
|
g := NewWithT(t)
|
|
|
|
ageID, err := extage.GenerateX25519Identity()
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
|
|
kd := &Decryptor{}
|
|
|
|
format := formats.Binary
|
|
encData, err := kd.sopsEncryptWithFormat(sops.Metadata{
|
|
KeyGroups: []sops.KeyGroup{
|
|
{&age.MasterKey{Recipient: ageID.Recipient().String()}},
|
|
},
|
|
}, []byte("foo bar"), format, format)
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
g.Expect(bytes.Contains(encData, sopsFormatToMarkerBytes[format])).To(BeTrue())
|
|
|
|
data, err := kd.SopsDecryptWithFormat(encData, format, format)
|
|
g.Expect(err).To(HaveOccurred())
|
|
g.Expect(err.Error()).To(ContainSubstring("cannot get sops data key"))
|
|
g.Expect(data).To(BeNil())
|
|
})
|
|
|
|
t.Run("with mac check", func(t *testing.T) {
|
|
g := NewWithT(t)
|
|
|
|
ageID, err := extage.GenerateX25519Identity()
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
|
|
kd := &Decryptor{
|
|
checkSopsMac: true,
|
|
ageIdentities: age.ParsedIdentities{ageID},
|
|
}
|
|
|
|
format := formats.Dotenv
|
|
data := []byte("key=value\n")
|
|
encData, err := kd.sopsEncryptWithFormat(sops.Metadata{
|
|
KeyGroups: []sops.KeyGroup{
|
|
{&age.MasterKey{Recipient: ageID.Recipient().String()}},
|
|
},
|
|
}, data, format, format)
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
g.Expect(bytes.Contains(encData, sopsFormatToMarkerBytes[format])).To(BeTrue())
|
|
|
|
out, err := kd.SopsDecryptWithFormat(encData, format, format)
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
g.Expect(out).To(Equal(data))
|
|
|
|
badMAC := regexp.MustCompile("(?m)[\r\n]+^.*sops_mac=.*$")
|
|
badMACData := badMAC.ReplaceAll(encData, []byte("\nsops_mac=\n"))
|
|
out, err = kd.SopsDecryptWithFormat(badMACData, format, format)
|
|
g.Expect(err).To(HaveOccurred())
|
|
g.Expect(err.Error()).To(ContainSubstring("failed to verify sops data integrity: expected mac 'no MAC'"))
|
|
g.Expect(out).To(BeNil())
|
|
})
|
|
}
|
|
|
|
func TestDecryptor_DecryptResource(t *testing.T) {
|
|
var (
|
|
resourceFactory = provider.NewDefaultDepProvider().GetResourceFactory()
|
|
emptyResource = resourceFactory.FromMap(map[string]interface{}{})
|
|
)
|
|
|
|
newSecretResource := func(namespace, name string, data map[string]interface{}) *resource.Resource {
|
|
return resourceFactory.FromMap(map[string]interface{}{
|
|
"apiVersion": "v1",
|
|
"kind": "Secret",
|
|
"metadata": map[string]interface{}{
|
|
"name": name,
|
|
"namespace": namespace,
|
|
},
|
|
"data": data,
|
|
})
|
|
}
|
|
|
|
kustomization := kustomizev1.Kustomization{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "decrypt",
|
|
Namespace: "decrypt",
|
|
},
|
|
Spec: kustomizev1.KustomizationSpec{
|
|
Interval: metav1.Duration{Duration: 2 * time.Minute},
|
|
Path: "./",
|
|
},
|
|
}
|
|
|
|
t.Run("SOPS-encrypted Secret resource", func(t *testing.T) {
|
|
g := NewWithT(t)
|
|
|
|
kus := kustomization.DeepCopy()
|
|
kus.Spec.Decryption = &kustomizev1.Decryption{
|
|
Provider: DecryptionProviderSOPS,
|
|
}
|
|
|
|
d, cleanup, err := NewTempDecryptor("", fake.NewClientBuilder().Build(), kus)
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
t.Cleanup(cleanup)
|
|
|
|
ageID, err := extage.GenerateX25519Identity()
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
d.ageIdentities = append(d.ageIdentities, ageID)
|
|
|
|
secret := newSecretResource("test", "secret", map[string]interface{}{
|
|
"key": "value",
|
|
})
|
|
g.Expect(isSOPSEncryptedResource(secret)).To(BeFalse())
|
|
|
|
secretData, err := secret.MarshalJSON()
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
|
|
encData, err := d.sopsEncryptWithFormat(sops.Metadata{
|
|
EncryptedRegex: "^(data|stringData)$",
|
|
KeyGroups: []sops.KeyGroup{
|
|
{&age.MasterKey{Recipient: ageID.Recipient().String()}},
|
|
},
|
|
}, secretData, formats.Json, formats.Json)
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
|
|
g.Expect(secret.UnmarshalJSON(encData)).To(Succeed())
|
|
g.Expect(isSOPSEncryptedResource(secret)).To(BeTrue())
|
|
|
|
got, err := d.DecryptResource(secret)
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
g.Expect(got).ToNot(BeNil())
|
|
g.Expect(got.MarshalJSON()).To(Equal(secretData))
|
|
})
|
|
|
|
t.Run("SOPS-encrypted binary-format Secret data field", func(t *testing.T) {
|
|
g := NewWithT(t)
|
|
|
|
kus := kustomization.DeepCopy()
|
|
kus.Spec.Decryption = &kustomizev1.Decryption{
|
|
Provider: DecryptionProviderSOPS,
|
|
}
|
|
|
|
d, cleanup, err := NewTempDecryptor("", fake.NewClientBuilder().Build(), kus)
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
t.Cleanup(cleanup)
|
|
|
|
ageID, err := extage.GenerateX25519Identity()
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
d.ageIdentities = append(d.ageIdentities, ageID)
|
|
|
|
plainData := []byte("[config]\napp = secret\n")
|
|
encData, err := d.sopsEncryptWithFormat(sops.Metadata{
|
|
KeyGroups: []sops.KeyGroup{
|
|
{&age.MasterKey{Recipient: ageID.Recipient().String()}},
|
|
},
|
|
}, plainData, formats.Ini, formats.Yaml)
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
|
|
secret := newSecretResource("test", "secret-data", map[string]interface{}{
|
|
"file.ini": base64.StdEncoding.EncodeToString(encData),
|
|
})
|
|
g.Expect(isSOPSEncryptedResource(secret)).To(BeFalse())
|
|
|
|
got, err := d.DecryptResource(secret)
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
g.Expect(got).ToNot(BeNil())
|
|
g.Expect(got.GetDataMap()).To(HaveKeyWithValue("file.ini", base64.StdEncoding.EncodeToString(plainData)))
|
|
})
|
|
|
|
t.Run("SOPS-encrypted YAML-format Secret data field", func(t *testing.T) {
|
|
g := NewWithT(t)
|
|
|
|
kus := kustomization.DeepCopy()
|
|
kus.Spec.Decryption = &kustomizev1.Decryption{
|
|
Provider: DecryptionProviderSOPS,
|
|
}
|
|
|
|
d, cleanup, err := NewTempDecryptor("", fake.NewClientBuilder().Build(), kus)
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
t.Cleanup(cleanup)
|
|
|
|
ageID, err := extage.GenerateX25519Identity()
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
d.ageIdentities = append(d.ageIdentities, ageID)
|
|
|
|
plainData := []byte("structured:\n data:\n key: value\n")
|
|
encData, err := d.sopsEncryptWithFormat(sops.Metadata{
|
|
KeyGroups: []sops.KeyGroup{
|
|
{&age.MasterKey{Recipient: ageID.Recipient().String()}},
|
|
},
|
|
}, plainData, formats.Yaml, formats.Yaml)
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
|
|
secret := newSecretResource("test", "secret-data", map[string]interface{}{
|
|
"key.yaml": base64.StdEncoding.EncodeToString(encData),
|
|
})
|
|
g.Expect(isSOPSEncryptedResource(secret)).To(BeFalse())
|
|
|
|
got, err := d.DecryptResource(secret)
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
g.Expect(got).ToNot(BeNil())
|
|
g.Expect(got.GetDataMap()).To(HaveKeyWithValue("key.yaml", base64.StdEncoding.EncodeToString(plainData)))
|
|
})
|
|
|
|
t.Run("SOPS-encrypted Docker config Secret", func(t *testing.T) {
|
|
g := NewWithT(t)
|
|
|
|
kus := kustomization.DeepCopy()
|
|
kus.Spec.Decryption = &kustomizev1.Decryption{
|
|
Provider: DecryptionProviderSOPS,
|
|
}
|
|
|
|
d, cleanup, err := NewTempDecryptor("", fake.NewClientBuilder().Build(), kus)
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
t.Cleanup(cleanup)
|
|
|
|
ageID, err := extage.GenerateX25519Identity()
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
d.ageIdentities = append(d.ageIdentities, ageID)
|
|
|
|
plainData := []byte(`{
|
|
"auths": {
|
|
"my-registry.example:5000": {
|
|
"username": "tiger",
|
|
"password": "pass1234",
|
|
"email": "tiger@acme.example",
|
|
"auth": "dGlnZXI6cGFzczEyMzQ="
|
|
}
|
|
}
|
|
}`)
|
|
encData, err := d.sopsEncryptWithFormat(sops.Metadata{
|
|
KeyGroups: []sops.KeyGroup{
|
|
{&age.MasterKey{Recipient: ageID.Recipient().String()}},
|
|
},
|
|
}, plainData, formats.Json, formats.Yaml)
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
|
|
secret := resourceFactory.FromMap(map[string]interface{}{
|
|
"apiVersion": "v1",
|
|
"kind": "Secret",
|
|
"metadata": map[string]interface{}{
|
|
"name": "secret",
|
|
"namespace": "test",
|
|
},
|
|
"type": corev1.SecretTypeDockerConfigJson,
|
|
"data": map[string]interface{}{
|
|
corev1.DockerConfigJsonKey: base64.StdEncoding.EncodeToString(encData),
|
|
},
|
|
})
|
|
g.Expect(isSOPSEncryptedResource(secret)).To(BeFalse())
|
|
|
|
got, err := d.DecryptResource(secret)
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
g.Expect(got).ToNot(BeNil())
|
|
g.Expect(got.GetDataMap()).To(HaveKeyWithValue(corev1.DockerConfigJsonKey, base64.StdEncoding.EncodeToString(plainData)))
|
|
})
|
|
|
|
t.Run("nil resource", func(t *testing.T) {
|
|
g := NewWithT(t)
|
|
|
|
d, cleanup, err := NewTempDecryptor("", fake.NewClientBuilder().Build(), kustomization.DeepCopy())
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
t.Cleanup(cleanup)
|
|
|
|
got, err := d.DecryptResource(nil)
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
g.Expect(got).To(BeNil())
|
|
})
|
|
|
|
t.Run("no decryption spec", func(t *testing.T) {
|
|
g := NewWithT(t)
|
|
|
|
d, cleanup, err := NewTempDecryptor("", fake.NewClientBuilder().Build(), kustomization.DeepCopy())
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
t.Cleanup(cleanup)
|
|
|
|
got, err := d.DecryptResource(emptyResource.DeepCopy())
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
g.Expect(got).To(BeNil())
|
|
})
|
|
|
|
t.Run("unimplemented decryption provider", func(t *testing.T) {
|
|
g := NewWithT(t)
|
|
|
|
kus := kustomization.DeepCopy()
|
|
kus.Spec.Decryption = &kustomizev1.Decryption{
|
|
Provider: "not-supported",
|
|
}
|
|
d, cleanup, err := NewTempDecryptor("", fake.NewClientBuilder().Build(), kus)
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
t.Cleanup(cleanup)
|
|
|
|
got, err := d.DecryptResource(emptyResource.DeepCopy())
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
g.Expect(got).To(BeNil())
|
|
})
|
|
}
|
|
|
|
func TestDecryptor_decryptKustomizationEnvSources(t *testing.T) {
|
|
type file struct {
|
|
name string
|
|
symlink string
|
|
data []byte
|
|
originalFormat *formats.Format
|
|
encrypt bool
|
|
expectData bool
|
|
}
|
|
binaryFormat := formats.Binary
|
|
tests := []struct {
|
|
name string
|
|
wordirSuffix string
|
|
path string
|
|
files []file
|
|
secretGenerator []kustypes.SecretArgs
|
|
expectVisited []string
|
|
wantErr error
|
|
}{
|
|
{
|
|
name: "decrypt env sources",
|
|
path: "subdir",
|
|
files: []file{
|
|
{name: "subdir/app.env", data: []byte("var1=value1\n"), encrypt: true, expectData: true},
|
|
// NB: Despite the file extension representing the SOPS-encrypted JSON output
|
|
// format, the original data is plain text, or "binary."
|
|
{name: "subdir/combination.json", data: []byte("The safe combination is ..."), originalFormat: &binaryFormat, encrypt: true, expectData: true},
|
|
{name: "subdir/file.txt", data: []byte("file"), encrypt: true, expectData: true},
|
|
{name: "secret.env", data: []byte("var2=value2\n"), encrypt: true, expectData: true},
|
|
},
|
|
secretGenerator: []kustypes.SecretArgs{
|
|
{
|
|
GeneratorArgs: kustypes.GeneratorArgs{
|
|
Name: "envSecret",
|
|
KvPairSources: kustypes.KvPairSources{
|
|
FileSources: []string{"file.txt", "combo=combination.json"},
|
|
EnvSources: []string{"app.env", "../secret.env"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
expectVisited: []string{"subdir/app.env", "subdir/combination.json", "subdir/file.txt", "secret.env"},
|
|
},
|
|
{
|
|
name: "decryption error",
|
|
files: []file{},
|
|
secretGenerator: []kustypes.SecretArgs{
|
|
{
|
|
GeneratorArgs: kustypes.GeneratorArgs{
|
|
Name: "envSecret",
|
|
KvPairSources: kustypes.KvPairSources{
|
|
EnvSources: []string{"file.txt"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
expectVisited: []string{},
|
|
wantErr: &fs.PathError{Op: "lstat", Path: "file.txt", Err: fmt.Errorf("")},
|
|
},
|
|
{
|
|
name: "follows relative symlink within root",
|
|
path: "subdir",
|
|
files: []file{
|
|
{name: "subdir/symlink", symlink: "../otherdir/data.env"},
|
|
{name: "otherdir/data.env", data: []byte("key=value\n"), encrypt: true, expectData: true},
|
|
},
|
|
secretGenerator: []kustypes.SecretArgs{
|
|
{
|
|
GeneratorArgs: kustypes.GeneratorArgs{
|
|
Name: "envSecret",
|
|
KvPairSources: kustypes.KvPairSources{
|
|
EnvSources: []string{"symlink"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
expectVisited: []string{"otherdir/data.env"},
|
|
},
|
|
{
|
|
name: "error on symlink outside root",
|
|
wordirSuffix: "subdir",
|
|
path: "./",
|
|
files: []file{
|
|
{name: "subdir/symlink", symlink: "../otherdir/data.env"},
|
|
{name: "otherdir/data.env", data: []byte("key=value\n"), encrypt: true, expectData: false},
|
|
},
|
|
secretGenerator: []kustypes.SecretArgs{
|
|
{
|
|
GeneratorArgs: kustypes.GeneratorArgs{
|
|
Name: "envSecret",
|
|
KvPairSources: kustypes.KvPairSources{
|
|
EnvSources: []string{"symlink"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
wantErr: &fs.PathError{Op: "lstat", Path: "otherdir/data.env", Err: fmt.Errorf("")},
|
|
expectVisited: []string{},
|
|
},
|
|
{
|
|
name: "error on reference outside root",
|
|
wordirSuffix: "subdir",
|
|
path: "./",
|
|
files: []file{
|
|
{name: "data.env", data: []byte("key=value\n"), encrypt: true, expectData: false},
|
|
},
|
|
secretGenerator: []kustypes.SecretArgs{
|
|
{
|
|
GeneratorArgs: kustypes.GeneratorArgs{
|
|
Name: "envSecret",
|
|
KvPairSources: kustypes.KvPairSources{
|
|
EnvSources: []string{"../data.env"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
wantErr: &fs.PathError{Op: "lstat", Path: "data.env", Err: fmt.Errorf("")},
|
|
expectVisited: []string{},
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
g := NewWithT(t)
|
|
|
|
tmpDir := t.TempDir()
|
|
root := filepath.Join(tmpDir, tt.wordirSuffix)
|
|
|
|
id, err := extage.GenerateX25519Identity()
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
ageIdentities := age.ParsedIdentities{id}
|
|
|
|
d := &Decryptor{
|
|
root: root,
|
|
ageIdentities: ageIdentities,
|
|
}
|
|
|
|
for _, f := range tt.files {
|
|
fPath := filepath.Join(tmpDir, f.name)
|
|
g.Expect(os.MkdirAll(filepath.Dir(fPath), 0o700)).To(Succeed())
|
|
if f.symlink != "" {
|
|
g.Expect(os.Symlink(f.symlink, fPath)).To(Succeed())
|
|
continue
|
|
}
|
|
data := f.data
|
|
if f.encrypt {
|
|
var format formats.Format
|
|
if f.originalFormat != nil {
|
|
format = *f.originalFormat
|
|
} else {
|
|
format = formats.FormatForPath(f.name)
|
|
}
|
|
data, err = d.sopsEncryptWithFormat(sops.Metadata{
|
|
KeyGroups: []sops.KeyGroup{
|
|
{&age.MasterKey{Recipient: id.Recipient().String()}},
|
|
},
|
|
}, f.data, format, format)
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
g.Expect(data).ToNot(Equal(f.data))
|
|
}
|
|
g.Expect(os.WriteFile(fPath, data, 0o600)).To(Succeed())
|
|
}
|
|
|
|
visited := make(map[string]struct{}, 0)
|
|
visit := d.decryptKustomizationEnvSources(visited)
|
|
kus := &kustypes.Kustomization{SecretGenerator: tt.secretGenerator}
|
|
|
|
err = visit(root, tt.path, kus)
|
|
if tt.wantErr == nil {
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
} else {
|
|
g.Expect(err).To(HaveOccurred())
|
|
g.Expect(err).To(BeAssignableToTypeOf(tt.wantErr))
|
|
g.Expect(err.Error()).To(ContainSubstring(tt.wantErr.Error()))
|
|
}
|
|
|
|
for _, f := range tt.files {
|
|
if f.symlink != "" {
|
|
continue
|
|
}
|
|
|
|
b, err := os.ReadFile(filepath.Join(tmpDir, f.name))
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
if f.expectData {
|
|
g.Expect(b).To(Equal(f.data))
|
|
} else {
|
|
g.Expect(b).ToNot(Equal(f.data))
|
|
}
|
|
}
|
|
|
|
absVisited := make(map[string]struct{}, 0)
|
|
for _, v := range tt.expectVisited {
|
|
absVisited[filepath.Join(tmpDir, v)] = struct{}{}
|
|
}
|
|
g.Expect(visited).To(Equal(absVisited))
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDecryptor_decryptSopsFile(t *testing.T) {
|
|
g := NewWithT(t)
|
|
|
|
id, err := extage.GenerateX25519Identity()
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
ageIdentities := age.ParsedIdentities{id}
|
|
|
|
type file struct {
|
|
name string
|
|
symlink string
|
|
data []byte
|
|
encrypt bool
|
|
format formats.Format
|
|
expectData bool
|
|
}
|
|
tests := []struct {
|
|
name string
|
|
ageIdentities age.ParsedIdentities
|
|
maxFileSize int64
|
|
files []file
|
|
path string
|
|
format formats.Format
|
|
wantErr error
|
|
}{
|
|
{
|
|
name: "decrypt dotenv file",
|
|
ageIdentities: age.ParsedIdentities{id},
|
|
files: []file{
|
|
{name: "app.env", data: []byte("app=key\n"), encrypt: true, format: formats.Dotenv, expectData: true},
|
|
},
|
|
path: "app.env",
|
|
format: formats.Dotenv,
|
|
},
|
|
{
|
|
name: "decrypt YAML file",
|
|
ageIdentities: age.ParsedIdentities{id},
|
|
files: []file{
|
|
{name: "app.yaml", data: []byte("app: key\n"), encrypt: true, format: formats.Yaml, expectData: true},
|
|
},
|
|
path: "app.yaml",
|
|
format: formats.Yaml,
|
|
},
|
|
{
|
|
name: "irregular file",
|
|
files: []file{},
|
|
wantErr: fmt.Errorf("cannot decrypt irregular file as it has file mode type bits set"),
|
|
},
|
|
{
|
|
name: "file exceeds max size",
|
|
maxFileSize: 5,
|
|
files: []file{
|
|
{name: "app.env", data: []byte("app=key\n"), encrypt: true, format: formats.Dotenv, expectData: false},
|
|
},
|
|
path: "app.env",
|
|
wantErr: fmt.Errorf("cannot decrypt file with size (972 bytes) exceeding limit (5)"),
|
|
},
|
|
{
|
|
name: "wrong file format",
|
|
files: []file{
|
|
{name: "app.ini", data: []byte("[app]\nkey = value"), encrypt: true, format: formats.Ini, expectData: false},
|
|
},
|
|
path: "app.ini",
|
|
},
|
|
{
|
|
name: "does not follow symlink",
|
|
files: []file{
|
|
{name: "link", symlink: "../"},
|
|
},
|
|
path: "link",
|
|
wantErr: fmt.Errorf("cannot decrypt irregular file as it has file mode type bits set"),
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
g := NewWithT(t)
|
|
|
|
tmpDir := t.TempDir()
|
|
|
|
d := &Decryptor{
|
|
root: tmpDir,
|
|
maxFileSize: maxEncryptedFileSize,
|
|
ageIdentities: ageIdentities,
|
|
}
|
|
if tt.maxFileSize != 0 {
|
|
d.maxFileSize = tt.maxFileSize
|
|
}
|
|
|
|
for _, f := range tt.files {
|
|
fPath := filepath.Join(tmpDir, f.name)
|
|
if f.symlink != "" {
|
|
g.Expect(os.Symlink(f.symlink, fPath)).To(Succeed())
|
|
continue
|
|
}
|
|
data := f.data
|
|
if f.encrypt {
|
|
b, err := d.sopsEncryptWithFormat(sops.Metadata{
|
|
KeyGroups: []sops.KeyGroup{
|
|
{&age.MasterKey{Recipient: id.Recipient().String()}},
|
|
},
|
|
}, data, f.format, f.format)
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
g.Expect(b).ToNot(Equal(f.data))
|
|
data = b
|
|
}
|
|
g.Expect(os.MkdirAll(filepath.Dir(fPath), 0o700)).To(Succeed())
|
|
g.Expect(os.WriteFile(fPath, data, 0o600)).To(Succeed())
|
|
}
|
|
|
|
path := filepath.Join(tmpDir, tt.path)
|
|
err := d.sopsDecryptFile(path, tt.format, tt.format)
|
|
if tt.wantErr != nil {
|
|
g.Expect(err).To(HaveOccurred())
|
|
g.Expect(err).To(BeAssignableToTypeOf(tt.wantErr))
|
|
g.Expect(err.Error()).To(ContainSubstring(tt.wantErr.Error()))
|
|
} else {
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
}
|
|
for _, f := range tt.files {
|
|
if f.symlink != "" {
|
|
continue
|
|
}
|
|
|
|
b, err := os.ReadFile(filepath.Join(tmpDir, f.name))
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
g.Expect(bytes.Equal(f.data, b)).To(Equal(f.expectData))
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDecryptor_secureLoadKustomizationFile(t *testing.T) {
|
|
kusType := kustypes.TypeMeta{
|
|
APIVersion: kustypes.KustomizationVersion,
|
|
Kind: kustypes.KustomizationKind,
|
|
}
|
|
type file struct {
|
|
name string
|
|
symlink string
|
|
data []byte
|
|
}
|
|
tests := []struct {
|
|
name string
|
|
rootSuffix string
|
|
files []file
|
|
path string
|
|
want *kustypes.Kustomization
|
|
wantErr error
|
|
}{
|
|
{
|
|
name: "loads default kustomization file",
|
|
files: []file{
|
|
{name: konfig.DefaultKustomizationFileName(), data: []byte("resources:\n- resource.yaml")},
|
|
},
|
|
path: "./",
|
|
want: &kustypes.Kustomization{
|
|
TypeMeta: kusType,
|
|
Resources: []string{"resource.yaml"},
|
|
},
|
|
},
|
|
{
|
|
name: "loads recognized kustomization file",
|
|
files: []file{
|
|
{name: konfig.RecognizedKustomizationFileNames()[1], data: []byte("resources:\n- resource.yaml")},
|
|
},
|
|
path: "./",
|
|
want: &kustypes.Kustomization{
|
|
TypeMeta: kusType,
|
|
Resources: []string{"resource.yaml"},
|
|
},
|
|
},
|
|
{
|
|
name: "error on ambitious file match",
|
|
files: []file{
|
|
{name: konfig.RecognizedKustomizationFileNames()[0], data: []byte("resources:\n- resource.yaml")},
|
|
{name: konfig.RecognizedKustomizationFileNames()[1], data: []byte("resources:\n- resource.yaml")},
|
|
},
|
|
path: "./",
|
|
wantErr: fmt.Errorf("found multiple kustomization files"),
|
|
},
|
|
{
|
|
name: "error on no file found",
|
|
files: []file{},
|
|
path: "./",
|
|
wantErr: fmt.Errorf("no kustomization file found"),
|
|
},
|
|
{
|
|
name: "error on symlink outside root",
|
|
rootSuffix: "subdir",
|
|
files: []file{
|
|
{name: konfig.DefaultKustomizationFileName(), data: []byte("resources:\n- resource.yaml")},
|
|
{name: "subdir/" + konfig.DefaultKustomizationFileName(), symlink: "../kustomization.yaml"},
|
|
},
|
|
wantErr: fmt.Errorf("no kustomization file found"),
|
|
},
|
|
{
|
|
name: "error on invalid file",
|
|
files: []file{
|
|
{name: konfig.DefaultKustomizationFileName(), data: []byte("resources")},
|
|
},
|
|
wantErr: fmt.Errorf("failed to unmarshal kustomization file"),
|
|
},
|
|
{
|
|
name: "error on absolute path",
|
|
path: "/absolute/",
|
|
wantErr: fmt.Errorf("path '/absolute/' must be relative"),
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
g := NewWithT(t)
|
|
|
|
tmpDir := t.TempDir()
|
|
for _, f := range tt.files {
|
|
fPath := filepath.Join(tmpDir, f.name)
|
|
if f.symlink != "" {
|
|
g.Expect(os.Symlink(f.symlink, fPath))
|
|
continue
|
|
}
|
|
g.Expect(os.MkdirAll(filepath.Dir(fPath), 0o700)).To(Succeed())
|
|
g.Expect(os.WriteFile(fPath, f.data, 0o600)).To(Succeed())
|
|
}
|
|
|
|
root := filepath.Join(tmpDir, tt.rootSuffix)
|
|
got, err := secureLoadKustomizationFile(root, tt.path)
|
|
if wantErr := tt.wantErr; wantErr != nil {
|
|
g.Expect(err).To(HaveOccurred())
|
|
g.Expect(err.Error()).To(ContainSubstring(wantErr.Error()))
|
|
g.Expect(got).To(BeNil())
|
|
return
|
|
}
|
|
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
g.Expect(got).To(Equal(tt.want))
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDecryptor_recurseKustomizationFiles(t *testing.T) {
|
|
type kusNode struct {
|
|
path string
|
|
symlink string
|
|
resources []string
|
|
visitErr error
|
|
visited int
|
|
expectVisited int
|
|
expectCached bool
|
|
}
|
|
tests := []struct {
|
|
name string
|
|
wordirSuffix string
|
|
path string
|
|
nodes []*kusNode
|
|
wantErr error
|
|
wantErrStr string
|
|
}{
|
|
{
|
|
name: "recurse on resources",
|
|
wordirSuffix: "foo",
|
|
path: "bar",
|
|
nodes: []*kusNode{
|
|
{
|
|
path: "foo/bar/kustomization.yaml",
|
|
resources: []string{"../baz"},
|
|
expectVisited: 1,
|
|
expectCached: true,
|
|
},
|
|
{
|
|
path: "foo/baz/kustomization.yaml",
|
|
resources: []string{"<tmpdir>/foo/bar/baz"},
|
|
expectVisited: 1,
|
|
expectCached: true,
|
|
},
|
|
{
|
|
path: "foo/bar/baz/kustomization.yaml",
|
|
resources: []string{},
|
|
expectVisited: 1,
|
|
expectCached: true,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "recursive loop",
|
|
wordirSuffix: "foo",
|
|
path: "bar",
|
|
nodes: []*kusNode{
|
|
{
|
|
path: "foo/bar/kustomization.yaml",
|
|
resources: []string{"../baz"},
|
|
expectVisited: 1,
|
|
expectCached: true,
|
|
},
|
|
{
|
|
path: "foo/baz/kustomization.yaml",
|
|
resources: []string{"../foobar"},
|
|
expectVisited: 1,
|
|
expectCached: true,
|
|
},
|
|
{
|
|
path: "foo/foobar/kustomization.yaml",
|
|
resources: []string{"../bar"},
|
|
expectVisited: 1,
|
|
expectCached: true,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "absolute symlink",
|
|
path: "bar",
|
|
nodes: []*kusNode{
|
|
{
|
|
path: "bar/baz/kustomization.yaml",
|
|
resources: []string{"../bar/absolute"},
|
|
expectVisited: 1,
|
|
expectCached: true,
|
|
},
|
|
{
|
|
path: "bar/absolute",
|
|
symlink: "<tmpdir>/bar/foo/",
|
|
},
|
|
{
|
|
path: "bar/foo/kustomization.yaml",
|
|
expectVisited: 1,
|
|
expectCached: true,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "relative symlink",
|
|
path: "bar",
|
|
nodes: []*kusNode{
|
|
{
|
|
path: "bar/baz/kustomization.yaml",
|
|
resources: []string{"../bar/relative"},
|
|
expectVisited: 1,
|
|
expectCached: true,
|
|
},
|
|
{
|
|
path: "bar/relative",
|
|
symlink: "../foo/",
|
|
},
|
|
{
|
|
path: "bar/foo/kustomization.yaml",
|
|
expectVisited: 1,
|
|
expectCached: true,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "recognized kustomization names",
|
|
path: "./",
|
|
nodes: []*kusNode{
|
|
{
|
|
path: konfig.RecognizedKustomizationFileNames()[1],
|
|
resources: []string{"bar"},
|
|
expectVisited: 1,
|
|
expectCached: true,
|
|
},
|
|
{
|
|
path: filepath.Join("bar", konfig.RecognizedKustomizationFileNames()[0]),
|
|
resources: []string{"../baz"},
|
|
expectVisited: 1,
|
|
expectCached: true,
|
|
},
|
|
{
|
|
path: filepath.Join("baz", konfig.RecognizedKustomizationFileNames()[2]),
|
|
expectVisited: 1,
|
|
expectCached: true,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "path does not exist",
|
|
path: "./invalid",
|
|
wantErr: &errRecurseIgnore{Err: fs.ErrNotExist},
|
|
wantErrStr: "lstat invalid",
|
|
},
|
|
{
|
|
name: "path is not a directory",
|
|
path: "./file.txt",
|
|
nodes: []*kusNode{
|
|
{
|
|
path: "file.txt",
|
|
},
|
|
},
|
|
wantErr: &errRecurseIgnore{Err: fmt.Errorf("not a directory")},
|
|
wantErrStr: "not a directory",
|
|
},
|
|
{
|
|
name: "recurse error is returned",
|
|
path: "/foo",
|
|
nodes: []*kusNode{
|
|
{
|
|
path: "foo/kustomization.yaml",
|
|
resources: []string{"../baz"},
|
|
expectVisited: 1,
|
|
expectCached: true,
|
|
},
|
|
{
|
|
path: "baz/wrongfile.yaml",
|
|
expectVisited: 0,
|
|
expectCached: false,
|
|
},
|
|
},
|
|
wantErr: fmt.Errorf("no kustomization file found"),
|
|
},
|
|
{
|
|
name: "recurse ignores errRecurseIgnore",
|
|
path: "/foo",
|
|
nodes: []*kusNode{
|
|
{
|
|
path: "foo/kustomization.yaml",
|
|
resources: []string{"../baz"},
|
|
expectVisited: 1,
|
|
expectCached: true,
|
|
},
|
|
{
|
|
path: "baz",
|
|
expectVisited: 0,
|
|
expectCached: false,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "remote build references are ignored",
|
|
path: "/foo",
|
|
nodes: []*kusNode{
|
|
{
|
|
path: "foo/kustomization.yaml",
|
|
resources: []string{
|
|
"../baz",
|
|
"https://github.com/kubernetes-sigs/kustomize//examples/multibases/dev/?ref=v1.0.6",
|
|
},
|
|
expectVisited: 1,
|
|
expectCached: true,
|
|
},
|
|
{
|
|
path: "baz/kustomization.yaml",
|
|
resources: []string{
|
|
"github.com/Liujingfang1/mysql?ref=test",
|
|
},
|
|
expectVisited: 1,
|
|
expectCached: true,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "visit error is returned",
|
|
path: "/",
|
|
nodes: []*kusNode{
|
|
{
|
|
path: "kustomization.yaml",
|
|
resources: []string{
|
|
"baz",
|
|
},
|
|
expectVisited: 1,
|
|
expectCached: true,
|
|
},
|
|
{
|
|
path: "baz/kustomization.yaml",
|
|
visitErr: fmt.Errorf("visit error"),
|
|
expectVisited: 1,
|
|
expectCached: true,
|
|
},
|
|
},
|
|
wantErr: fmt.Errorf("visit error"),
|
|
wantErrStr: "visit error",
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
g := NewWithT(t)
|
|
|
|
tmpDir := t.TempDir()
|
|
|
|
for _, n := range tt.nodes {
|
|
path := filepath.Join(tmpDir, n.path)
|
|
if n.symlink != "" {
|
|
g.Expect(os.Symlink(strings.Replace(n.symlink, "<tmpdir>", tmpDir, 1), path)).To(Succeed())
|
|
return
|
|
}
|
|
kus := kustypes.Kustomization{
|
|
TypeMeta: kustypes.TypeMeta{
|
|
APIVersion: kustypes.KustomizationVersion,
|
|
Kind: kustypes.KustomizationKind,
|
|
},
|
|
}
|
|
for _, res := range n.resources {
|
|
res = strings.Replace(res, "<tmpdir>", tmpDir, 1)
|
|
kus.Resources = append(kus.Resources, res)
|
|
}
|
|
b, err := yaml.Marshal(kus)
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
g.Expect(os.MkdirAll(filepath.Dir(path), 0o700)).To(Succeed())
|
|
g.Expect(os.WriteFile(path, b, 0o600))
|
|
}
|
|
|
|
visit := func(root, path string, kus *kustypes.Kustomization) error {
|
|
if filepath.IsAbs(path) {
|
|
path = stripRoot(root, path)
|
|
}
|
|
for _, n := range tt.nodes {
|
|
if dir := filepath.Dir(n.path); filepath.Join(tt.wordirSuffix, path) != dir {
|
|
continue
|
|
}
|
|
n.visited++
|
|
if n.visitErr != nil {
|
|
return n.visitErr
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
visited := make(map[string]struct{}, 0)
|
|
err := recurseKustomizationFiles(filepath.Join(tmpDir, tt.wordirSuffix), tt.path, visit, visited)
|
|
if tt.wantErr != nil {
|
|
g.Expect(err).To(HaveOccurred())
|
|
g.Expect(err).To(BeAssignableToTypeOf(tt.wantErr))
|
|
if tt.wantErrStr != "" {
|
|
g.Expect(err.Error()).To(ContainSubstring(tt.wantErrStr))
|
|
}
|
|
return
|
|
}
|
|
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
for _, n := range tt.nodes {
|
|
g.Expect(n.visited).To(Equal(n.expectVisited), n.path)
|
|
|
|
haveCache := HaveKey(filepath.Dir(filepath.Join(tmpDir, n.path)))
|
|
if n.expectCached {
|
|
g.Expect(visited).To(haveCache)
|
|
} else {
|
|
g.Expect(visited).ToNot(haveCache)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDecryptor_isSOPSEncryptedResource(t *testing.T) {
|
|
g := NewWithT(t)
|
|
|
|
resourceFactory := provider.NewDefaultDepProvider().GetResourceFactory()
|
|
encrypted := resourceFactory.FromMap(map[string]interface{}{
|
|
"sops": map[string]string{
|
|
"mac": "some mac value",
|
|
},
|
|
})
|
|
empty := resourceFactory.FromMap(map[string]interface{}{})
|
|
|
|
g.Expect(isSOPSEncryptedResource(encrypted)).To(BeTrue())
|
|
g.Expect(isSOPSEncryptedResource(empty)).To(BeFalse())
|
|
}
|
|
|
|
func TestDecryptor_secureAbsPath(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
root string
|
|
path string
|
|
wantAbs string
|
|
wantRel string
|
|
wantErr bool
|
|
}{
|
|
{
|
|
name: "absolute to root",
|
|
root: "/wordir/",
|
|
path: "/wordir/foo/",
|
|
wantAbs: "/wordir/foo",
|
|
wantRel: "foo",
|
|
},
|
|
{
|
|
name: "relative to root",
|
|
root: "/wordir",
|
|
path: "./foo",
|
|
wantAbs: "/wordir/foo",
|
|
wantRel: "foo",
|
|
},
|
|
{
|
|
name: "illegal traverse",
|
|
root: "/wordir/foo",
|
|
path: "../../bar",
|
|
wantAbs: "/wordir/foo/bar",
|
|
wantRel: "bar",
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
g := NewWithT(t)
|
|
|
|
gotAbs, gotRel, err := securePaths(tt.root, tt.path)
|
|
if tt.wantErr {
|
|
g.Expect(err).To(HaveOccurred())
|
|
g.Expect(gotAbs).To(BeEmpty())
|
|
g.Expect(gotRel).To(BeEmpty())
|
|
return
|
|
}
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
g.Expect(gotAbs).To(Equal(tt.wantAbs))
|
|
g.Expect(gotRel).To(Equal(tt.wantRel))
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDecryptor_formatForPath(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
path string
|
|
want formats.Format
|
|
}{
|
|
{
|
|
name: "docker config",
|
|
path: corev1.DockerConfigJsonKey,
|
|
want: formats.Json,
|
|
},
|
|
{
|
|
name: "fallback",
|
|
path: "foo.yaml",
|
|
want: formats.Yaml,
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
g := NewWithT(t)
|
|
|
|
g.Expect(formatForPath(tt.path)).To(Equal(tt.want))
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDecryptor_detectFormatFromMarkerBytes(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
b []byte
|
|
want formats.Format
|
|
}{
|
|
{
|
|
name: "detects format",
|
|
b: bytes.Join([][]byte{[]byte("random other bytes"), sopsFormatToMarkerBytes[formats.Yaml], []byte("more random bytes")}, []byte(" ")),
|
|
want: formats.Yaml,
|
|
},
|
|
{
|
|
name: "returns unsupported format",
|
|
b: []byte("no marker bytes present"),
|
|
want: unsupportedFormat,
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
if got := detectFormatFromMarkerBytes(tt.b); got != tt.want {
|
|
t.Errorf("detectFormatFromMarkerBytes() = %v, want %v", got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|