diff --git a/api/v1beta1/emergencyaccount_types.go b/api/v1beta1/emergencyaccount_types.go index cb932b6..ae86a28 100644 --- a/api/v1beta1/emergencyaccount_types.go +++ b/api/v1beta1/emergencyaccount_types.go @@ -52,10 +52,10 @@ type TokenStoreSpec struct { // +kubebuilder:validation:Required Name string `json:"name"` // Type defines the type of the store to use. - // Currently `secret`` and `log` stores are supported. + // Currently `secret`, `s3`, and `log` stores are supported. // The stores can be further configured in the corresponding storeSpec. // +kubebuilder:validation:Required - // +kubebuilder:validation:Enum=secret;log + // +kubebuilder:validation:Enum=secret;log;s3 Type string `json:"type"` // SecretSpec configures the secret store. @@ -64,6 +64,58 @@ type TokenStoreSpec struct { // LogSpec configures the log store. // The log store outputs the token to the log but does not store it anywhere. LogSpec LogStoreSpec `json:"logStore,omitempty"` + // S3Spec configures the S3 store. + // The S3 store saves the tokens in an S3 bucket. + S3Spec S3StoreSpec `json:"s3Store,omitempty"` +} + +// S3StoreSpec configures the S3 store. +// The S3 store saves the tokens in an S3 bucket with optional encryption using PGP public keys. +type S3StoreSpec struct { + // ObjectNameTemplate is the template for the object name to use. + // Sprig functions can be used to generate the object name. + // If not set, the object name is the name of the EmergencyAccount. + // The name of the EmergencyAccount can be accessed with `{{ .Name }}`. + // The namespace of the EmergencyAccount can be accessed with `{{ .Namespace }}`. + // The full EmergencyAccount object can be accessed with `{{ .EmergencyAccount }}`. + // Additional context can be passed with the `objectNameTemplateContext` field and is accessible with `{{ .Context. }}`. + // +kubebuilder:validation:Optional + ObjectNameTemplate string `json:"objectNameTemplate,omitempty"` + // ObjectNameTemplateContext is the additional context to use for the object name template. + // +kubebuilder:validation:Optional + ObjectNameTemplateContext map[string]string `json:"objectNameTemplateContext,omitempty"` + + S3 S3Spec `json:"s3"` + // Encryption defines the encryption settings for the S3 store. + // If not set, the tokens are stored unencrypted. + // +kubebuilder:validation:Optional + Encryption S3EncryptionSpec `json:"encryption,omitempty"` +} + +type S3Spec struct { + // Endpoint is the S3 endpoint to use. + Endpoint string `json:"endpoint"` + // Bucket is the S3 bucket to use. + Bucket string `json:"bucket"` + + // AccessKeyId and SecretAccessKey are the S3 credentials to use. + AccessKeyId string `json:"accessKeyId"` + // SecretAccessKey is the S3 secret access key to use. + SecretAccessKey string `json:"secretAccessKey"` + + // Region is the AWS region to use. + Region string `json:"region,omitempty"` + // Insecure allows to use an insecure connection to the S3 endpoint. + Insecure bool `json:"insecure,omitempty"` +} + +type S3EncryptionSpec struct { + // Encrypt defines if the tokens should be encrypted. + // If not set, the tokens are stored unencrypted. + Encrypt bool `json:"encrypt,omitempty"` + // PGPKeys is a list of PGP public keys to encrypt the tokens with. + // At least one key must be given if encryption is enabled. + PGPKeys []string `json:"pgpKeys,omitempty"` } // SecretStoreSpec configures the secret store. diff --git a/api/v1beta1/zz_generated.deepcopy.go b/api/v1beta1/zz_generated.deepcopy.go index 39c983d..2486125 100644 --- a/api/v1beta1/zz_generated.deepcopy.go +++ b/api/v1beta1/zz_generated.deepcopy.go @@ -77,7 +77,9 @@ func (in *EmergencyAccountSpec) DeepCopyInto(out *EmergencyAccountSpec) { if in.TokenStores != nil { in, out := &in.TokenStores, &out.TokenStores *out = make([]TokenStoreSpec, len(*in)) - copy(*out, *in) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } } } @@ -129,6 +131,65 @@ func (in *LogStoreSpec) DeepCopy() *LogStoreSpec { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *S3EncryptionSpec) DeepCopyInto(out *S3EncryptionSpec) { + *out = *in + if in.PGPKeys != nil { + in, out := &in.PGPKeys, &out.PGPKeys + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new S3EncryptionSpec. +func (in *S3EncryptionSpec) DeepCopy() *S3EncryptionSpec { + if in == nil { + return nil + } + out := new(S3EncryptionSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *S3Spec) DeepCopyInto(out *S3Spec) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new S3Spec. +func (in *S3Spec) DeepCopy() *S3Spec { + if in == nil { + return nil + } + out := new(S3Spec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *S3StoreSpec) DeepCopyInto(out *S3StoreSpec) { + *out = *in + if in.ObjectNameTemplateContext != nil { + in, out := &in.ObjectNameTemplateContext, &out.ObjectNameTemplateContext + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + out.S3 = in.S3 + in.Encryption.DeepCopyInto(&out.Encryption) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new S3StoreSpec. +func (in *S3StoreSpec) DeepCopy() *S3StoreSpec { + if in == nil { + return nil + } + out := new(S3StoreSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *SecretStoreSpec) DeepCopyInto(out *SecretStoreSpec) { *out = *in @@ -185,6 +246,7 @@ func (in *TokenStoreSpec) DeepCopyInto(out *TokenStoreSpec) { *out = *in out.SecretSpec = in.SecretSpec out.LogSpec = in.LogSpec + in.S3Spec.DeepCopyInto(&out.S3Spec) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TokenStoreSpec. diff --git a/config/crd/bases/cluster.appuio.io_emergencyaccounts.yaml b/config/crd/bases/cluster.appuio.io_emergencyaccounts.yaml index c2a48b8..49721d1 100644 --- a/config/crd/bases/cluster.appuio.io_emergencyaccounts.yaml +++ b/config/crd/bases/cluster.appuio.io_emergencyaccounts.yaml @@ -68,6 +68,76 @@ spec: description: Name is the name of the store. Must be unique within the EmergencyAccount type: string + s3Store: + description: S3Spec configures the S3 store. The S3 store saves + the tokens in an S3 bucket. + properties: + encryption: + description: Encryption defines the encryption settings + for the S3 store. If not set, the tokens are stored unencrypted. + properties: + encrypt: + description: Encrypt defines if the tokens should be + encrypted. If not set, the tokens are stored unencrypted. + type: boolean + pgpKeys: + description: PGPKeys is a list of PGP public keys to + encrypt the tokens with. At least one key must be + given if encryption is enabled. + items: + type: string + type: array + type: object + objectNameTemplate: + description: ObjectNameTemplate is the template for the + object name to use. Sprig functions can be used to generate + the object name. If not set, the object name is the name + of the EmergencyAccount. The name of the EmergencyAccount + can be accessed with `{{ .Name }}`. The namespace of the + EmergencyAccount can be accessed with `{{ .Namespace }}`. + The full EmergencyAccount object can be accessed with + `{{ .EmergencyAccount }}`. Additional context can be passed + with the `objectNameTemplateContext` field and is accessible + with `{{ .Context. }}`. + type: string + objectNameTemplateContext: + additionalProperties: + type: string + description: ObjectNameTemplateContext is the additional + context to use for the object name template. + type: object + s3: + properties: + accessKeyId: + description: AccessKeyId and SecretAccessKey are the + S3 credentials to use. + type: string + bucket: + description: Bucket is the S3 bucket to use. + type: string + endpoint: + description: Endpoint is the S3 endpoint to use. + type: string + insecure: + description: Insecure allows to use an insecure connection + to the S3 endpoint. + type: boolean + region: + description: Region is the AWS region to use. + type: string + secretAccessKey: + description: SecretAccessKey is the S3 secret access + key to use. + type: string + required: + - accessKeyId + - bucket + - endpoint + - secretAccessKey + type: object + required: + - s3 + type: object secretStore: description: SecretSpec configures the secret store. The secret store saves the tokens in a secret in the same namespace as @@ -75,11 +145,12 @@ spec: type: object type: description: Type defines the type of the store to use. Currently - `secret`` and `log` stores are supported. The stores can be - further configured in the corresponding storeSpec. + `secret`, `s3`, and `log` stores are supported. The stores + can be further configured in the corresponding storeSpec. enum: - secret - log + - s3 type: string required: - name diff --git a/controllers/stores/s3_store.go b/controllers/stores/s3_store.go new file mode 100644 index 0000000..e6e7c09 --- /dev/null +++ b/controllers/stores/s3_store.go @@ -0,0 +1,146 @@ +package stores + +import ( + "context" + "encoding/json" + "fmt" + "io" + "strings" + "text/template" + + "github.com/Masterminds/sprig/v3" + "github.com/ProtonMail/gopenpgp/v2/helper" + "github.com/appuio/emergency-credentials-controller/pkg/utils" + "github.com/minio/minio-go/v7" + "github.com/minio/minio-go/v7/pkg/credentials" + "go.uber.org/multierr" + + emcv1beta1 "github.com/appuio/emergency-credentials-controller/api/v1beta1" +) + +// MinioClient partially implements the minio.Client interface. +type MinioClient interface { + PutObject(ctx context.Context, bucketName string, objectName string, reader io.Reader, objectSize int64, opts minio.PutObjectOptions) (info minio.UploadInfo, err error) +} + +type S3Store struct { + minioClientFactory func(emcv1beta1.S3StoreSpec) (MinioClient, error) + spec emcv1beta1.S3StoreSpec +} + +var _ TokenStorer = &S3Store{} + +// NewS3Store creates a new S3Store +func NewS3Store(spec emcv1beta1.S3StoreSpec) *S3Store { + return NewS3StoreWithClientFactory(spec, DefaultClientFactory) +} + +// NewS3StoreWithClientFactory creates a new S3Store with the given client factory. +func NewS3StoreWithClientFactory(spec emcv1beta1.S3StoreSpec, minioClientFactory func(emcv1beta1.S3StoreSpec) (MinioClient, error)) *S3Store { + return &S3Store{spec: spec, minioClientFactory: minioClientFactory} +} + +// DefaultClientFactory is the default factory for creating a MinioClient. +func DefaultClientFactory(spec emcv1beta1.S3StoreSpec) (MinioClient, error) { + return minio.New(spec.S3.Endpoint, &minio.Options{ + Creds: credentials.NewStaticV4(spec.S3.AccessKeyId, spec.S3.SecretAccessKey, ""), + Secure: !spec.S3.Insecure, + Region: spec.S3.Region, + }) +} + +// StoreToken stores the token in the S3 bucket. +// If encryption is enabled, the token is encrypted with the given PGP public keys. +func (ss *S3Store) StoreToken(ctx context.Context, ea emcv1beta1.EmergencyAccount, token string) (string, error) { + objectname := ea.Name + if ss.spec.ObjectNameTemplate != "" { + t, err := template.New("fileName").Funcs(sprig.TxtFuncMap()).Parse(ss.spec.ObjectNameTemplate) + if err != nil { + return "", fmt.Errorf("unable to parse file name template: %w", err) + } + buf := new(strings.Builder) + if err := t.Execute(buf, struct { + Name string + Namespace string + EmergencyAccount emcv1beta1.EmergencyAccount + Context map[string]string + }{ + Name: ea.Name, + Namespace: ea.Namespace, + EmergencyAccount: ea, + Context: ss.spec.ObjectNameTemplateContext, + }); err != nil { + return "", fmt.Errorf("unable to execute file name template: %w", err) + } + objectname = buf.String() + } + + cli, err := ss.minioClientFactory(ss.spec) + if err != nil { + return "", fmt.Errorf("unable to create S3 client: %w", err) + } + + if ss.spec.Encryption.Encrypt { + token, err = encrypt(token, ss.spec.Encryption.PGPKeys) + if err != nil { + return "", fmt.Errorf("unable to encrypt token: %w", err) + } + } + + tr := strings.NewReader(token) + info, err := cli.PutObject(ctx, ss.spec.S3.Bucket, objectname, tr, int64(tr.Len()), minio.PutObjectOptions{}) + if err != nil { + return "", fmt.Errorf("unable to store token: %w", err) + } + + return info.Key, nil +} + +// EncryptedToken is the JSON structure of an encrypted token. +type EncryptedToken struct { + Secrets []EncryptedTokenSecret `json:"secrets"` +} + +// EncryptedTokenSecret is the JSON structure of an encrypted token secret. +type EncryptedTokenSecret struct { + Data string `json:"data"` +} + +// encrypt encrypts the token with the given PGP public keys. +// The token is encrypted with each key and the resulting encrypted tokens are returned as a JSON array. +func encrypt(token string, pgpKeys []string) (string, error) { + if len(pgpKeys) == 0 { + return "", fmt.Errorf("no PGP public keys given") + } + keys := []string{} + for _, key := range pgpKeys { + sk, err := utils.SplitPublicKeyBlocks(key) + if err != nil { + return "", fmt.Errorf("unable to parse PGP public key: %w", err) + } + keys = append(keys, sk...) + } + + encrypted := make([]EncryptedTokenSecret, 0, len(keys)) + errs := []error{} + for _, key := range keys { + enc, err := helper.EncryptMessageArmored((key), token) + if err != nil { + errs = append(errs, err) + continue + } + encrypted = append(encrypted, EncryptedTokenSecret{Data: enc}) + } + if multierr.Combine(errs...) != nil { + return "", fmt.Errorf("unable to fully encrypt token: %w", multierr.Combine(errs...)) + } + + s, err := json.Marshal(EncryptedToken{ + Secrets: encrypted, + }) + if err != nil { + return "", fmt.Errorf("unable to marshal encrypted token: %w", err) + } + + return string(s), nil +} diff --git a/controllers/stores/s3_store_test.go b/controllers/stores/s3_store_test.go new file mode 100644 index 0000000..65b9bdf --- /dev/null +++ b/controllers/stores/s3_store_test.go @@ -0,0 +1,162 @@ +package stores_test + +import ( + "context" + "crypto/sha256" + "encoding/json" + "fmt" + "io" + "strings" + "testing" + + "github.com/ProtonMail/gopenpgp/v2/crypto" + "github.com/ProtonMail/gopenpgp/v2/helper" + emcv1beta1 "github.com/appuio/emergency-credentials-controller/api/v1beta1" + "github.com/appuio/emergency-credentials-controller/controllers/stores" + "github.com/minio/minio-go/v7" + "github.com/stretchr/testify/require" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func Test_S3Store_StoreToken(t *testing.T) { + const ( + token = "token" + bucket = "bucket" + object = "object" + passphrase = "passphrase" + ) + + t.Run("without encryption", func(t *testing.T) { + mm := &MinioMock{} + st := stores.NewS3StoreWithClientFactory(emcv1beta1.S3StoreSpec{ + S3: emcv1beta1.S3Spec{ + Bucket: bucket, + }, + ObjectNameTemplate: "em-{{ .Name | sha256sum }}", + }, mm.ClientFactory) + + _, err := st.StoreToken(context.Background(), emcv1beta1.EmergencyAccount{ + ObjectMeta: metav1.ObjectMeta{ + Name: object, + }, + }, token) + require.NoError(t, err) + stored := mm.get(bucket, fmt.Sprintf("em-%x", sha256.Sum256([]byte(object)))) + require.NotEmpty(t, stored) + require.Equal(t, []byte(token), stored) + }) + + t.Run("encrypted", func(t *testing.T) { + privk1, pubk1, err := generateKeyPair("test1", "test1@test.ch", passphrase, "rsa", 2048) + require.NoError(t, err) + privk2, pubk2, err := generateKeyPair("test2", "test2@test.ch", passphrase, "rsa", 2048) + require.NoError(t, err) + privk3, pubk3, err := generateKeyPair("test3", "test3@test.ch", passphrase, "rsa", 2048) + require.NoError(t, err) + + mm := &MinioMock{} + st := stores.NewS3StoreWithClientFactory(emcv1beta1.S3StoreSpec{ + S3: emcv1beta1.S3Spec{ + Bucket: bucket, + }, + Encryption: emcv1beta1.S3EncryptionSpec{ + Encrypt: true, + PGPKeys: []string{strings.Join([]string{pubk1, pubk2}, "\n"), pubk3}, + }, + }, mm.ClientFactory) + + _, err = st.StoreToken(context.Background(), emcv1beta1.EmergencyAccount{ + ObjectMeta: metav1.ObjectMeta{ + Name: object, + }, + }, token) + require.NoError(t, err) + requireDecryptAll(t, string(mm.get(bucket, object)), token, passphrase, []string{privk1, privk2, privk3}) + }) +} + +func requireDecryptAll(t *testing.T, token, expectedMsg, passphrase string, keys []string) { + t.Helper() + + var data stores.EncryptedToken + err := json.Unmarshal([]byte(token), &data) + require.NoError(t, err) + + for _, secret := range data.Secrets { + requireDecrypt(t, secret.Data, expectedMsg, passphrase, keys) + } +} + +func requireDecrypt(t *testing.T, encrypted, expectedMsg, passphrase string, keys []string) { + t.Helper() + + for _, key := range keys { + msg, err := helper.DecryptMessageArmored(key, []byte(passphrase), encrypted) + if err == nil { + require.Equal(t, expectedMsg, string(msg)) + return + } + } + require.Fail(t, "expected to decrypt token with one of the given private keys") +} + +type MinioMock struct { + files map[string]map[string][]byte +} + +// ClientFactory returns itself. +func (mm *MinioMock) ClientFactory(emcv1beta1.S3StoreSpec) (stores.MinioClient, error) { + return mm, nil +} + +// PutObject implements the MinioClient interface. +func (mm *MinioMock) PutObject(ctx context.Context, bucketName string, objectName string, reader io.Reader, objectSize int64, opts minio.PutObjectOptions) (info minio.UploadInfo, err error) { + info.Bucket = bucketName + info.Key = objectName + + if mm.files == nil { + mm.files = make(map[string]map[string][]byte) + } + if mm.files[bucketName] == nil { + mm.files[bucketName] = make(map[string][]byte) + } + buf := make([]byte, objectSize) + _, err = reader.Read(buf) + if err != nil { + return info, err + } + mm.files[bucketName][objectName] = buf + return info, nil +} + +func (mm *MinioMock) get(bucketName string, objectName string) []byte { + if mm.files == nil { + return nil + } + + if mm.files[bucketName] == nil { + return nil + } + + return mm.files[bucketName][objectName] +} + +// generateKeyPair generates a key pair and returns the private and public key. +func generateKeyPair(name, email, passphrase string, keyType string, bits int) (privateKey string, publicKey string, err error) { + privateKey, err = helper.GenerateKey(name, email, []byte(passphrase), keyType, bits) + if err != nil { + return "", "", err + } + + ring, err := crypto.NewKeyFromArmoredReader(strings.NewReader(privateKey)) + if err != nil { + return "", "", err + } + + publicKey, err = ring.GetArmoredPublicKey() + if err != nil { + return "", "", err + } + + return privateKey, publicKey, nil +} diff --git a/controllers/stores/store.go b/controllers/stores/store.go index d8dd501..65849ea 100644 --- a/controllers/stores/store.go +++ b/controllers/stores/store.go @@ -27,5 +27,8 @@ func FromSpec(sts emcv1beta1.TokenStoreSpec) (TokenStorer, error) { if sts.Type == "log" { return NewLogStore(sts.LogSpec), nil } + if sts.Type == "s3" { + return NewS3Store(sts.S3Spec), nil + } return nil, fmt.Errorf("unknown token store type %s", sts.Type) } diff --git a/go.mod b/go.mod index 94afa7e..cb2a6fe 100644 --- a/go.mod +++ b/go.mod @@ -3,10 +3,14 @@ module github.com/appuio/emergency-credentials-controller go 1.21.5 require ( + github.com/Masterminds/sprig/v3 v3.2.3 + github.com/ProtonMail/gopenpgp/v2 v2.7.4 github.com/go-logr/logr v1.3.0 github.com/golang-jwt/jwt/v5 v5.2.0 + github.com/minio/minio-go/v7 v7.0.66 github.com/prometheus/client_golang v1.17.0 github.com/stretchr/testify v1.8.4 + go.uber.org/multierr v1.11.0 golang.org/x/exp v0.0.0-20231219180239-dc181d75b848 k8s.io/api v0.29.0 k8s.io/apimachinery v0.29.0 @@ -18,9 +22,15 @@ require ( ) require ( + github.com/Masterminds/goutils v1.1.1 // indirect + github.com/Masterminds/semver/v3 v3.2.0 // indirect + github.com/ProtonMail/go-crypto v0.0.0-20230717121422-5aa5874ade95 // indirect + github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/cloudflare/circl v1.3.3 // indirect github.com/davecgh/go-spew v1.1.1 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect github.com/emicklei/go-restful/v3 v3.11.0 // indirect github.com/evanphx/json-patch v5.7.0+incompatible // indirect github.com/evanphx/json-patch/v5 v5.7.0 // indirect @@ -40,14 +50,21 @@ require ( github.com/google/gofuzz v1.2.0 // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect github.com/google/uuid v1.5.0 // indirect + github.com/huandu/xstrings v1.3.3 // indirect github.com/imdario/mergo v0.3.16 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/compress v1.17.4 // indirect + github.com/klauspost/cpuid/v2 v2.2.6 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect + github.com/minio/md5-simd v1.1.2 // indirect + github.com/minio/sha256-simd v1.0.1 // indirect + github.com/mitchellh/copystructure v1.0.0 // indirect + github.com/mitchellh/reflectwalk v1.0.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect @@ -57,12 +74,16 @@ require ( github.com/prometheus/client_model v0.5.0 // indirect github.com/prometheus/common v0.45.0 // indirect github.com/prometheus/procfs v0.12.0 // indirect + github.com/rs/xid v1.5.0 // indirect + github.com/shopspring/decimal v1.2.0 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect + github.com/spf13/cast v1.3.1 // indirect github.com/spf13/cobra v1.8.0 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/xlab/treeprint v1.2.0 // indirect go.starlark.net v0.0.0-20231121155337-90ade8b19d09 // indirect - go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.26.0 // indirect + golang.org/x/crypto v0.16.0 // indirect golang.org/x/mod v0.14.0 // indirect golang.org/x/net v0.19.0 // indirect golang.org/x/oauth2 v0.15.0 // indirect @@ -76,6 +97,7 @@ require ( google.golang.org/protobuf v1.31.0 // indirect gopkg.in/evanphx/json-patch.v5 v5.7.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/apiextensions-apiserver v0.29.0 // indirect diff --git a/go.sum b/go.sum index 358db47..7fa96ee 100644 --- a/go.sum +++ b/go.sum @@ -1,11 +1,28 @@ +github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= +github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= +github.com/Masterminds/semver/v3 v3.2.0 h1:3MEsd0SM6jqZojhjLWWeBY+Kcjy9i6MQAeY7YgDP83g= +github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= +github.com/Masterminds/sprig/v3 v3.2.3 h1:eL2fZNezLomi0uOLqjQoN6BfsDD+fyLtgbJMAj9n6YA= +github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBaRMhvYXJNkGuM= +github.com/ProtonMail/go-crypto v0.0.0-20230717121422-5aa5874ade95 h1:KLq8BE0KwCL+mmXnjLWEAOYO+2l2AE4YMmqG1ZpZHBs= +github.com/ProtonMail/go-crypto v0.0.0-20230717121422-5aa5874ade95/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0= +github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f h1:tCbYj7/299ekTTXpdwKYF8eBlsYsDVoggDAuAjoK66k= +github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f/go.mod h1:gcr0kNtGBqin9zDW9GOHcVntrwnjrK+qdJ06mWYBybw= +github.com/ProtonMail/gopenpgp/v2 v2.7.4 h1:Vz/8+HViFFnf2A6XX8JOvZMrA6F5puwNvvF21O1mRlo= +github.com/ProtonMail/gopenpgp/v2 v2.7.4/go.mod h1:IhkNEDaxec6NyzSI0PlxapinnwPVIESk8/76da3Ct3g= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cloudflare/circl v1.3.3 h1:fE/Qz0QdIGqeWfnwq0RE0R7MI51s0M2E4Ga9kq5AEMs= +github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/evanphx/json-patch v5.7.0+incompatible h1:vgGkfT/9f8zE6tvSCe74nfpAVDQ2tG6yudJd8LBksgI= @@ -55,8 +72,12 @@ github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 h1:K6RDEckDVWvDI9JAJY github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= +github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU= github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/huandu/xstrings v1.3.3 h1:/Gcsuc1x8JVbJ9/rlye4xZnVAbEkGauT8lbebqcQws4= +github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4= github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= @@ -67,6 +88,11 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4= +github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= +github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.6 h1:ndNyv040zDGIDh8thGkXYjnFtiN02M1PVVF+JE/48xc= +github.com/klauspost/cpuid/v2 v2.2.6/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= @@ -80,6 +106,16 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg= github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k= +github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34= +github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM= +github.com/minio/minio-go/v7 v7.0.66 h1:bnTOXOHjOqv/gcMuiVbN9o2ngRItvqE774dG9nq0Dzw= +github.com/minio/minio-go/v7 v7.0.66/go.mod h1:DHAgmyQEGdW3Cif0UooKOyrT3Vxs82zNdV6tkKhRtbs= +github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM= +github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8= +github.com/mitchellh/copystructure v1.0.0 h1:Laisrj+bAB6b/yJwB5Bt3ITZhGJdqmxquMKeZ+mmkFQ= +github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= +github.com/mitchellh/reflectwalk v1.0.0 h1:9D+8oIskB4VJBN5SFlmc27fSlIBZaov1Wpk/IfikLNY= +github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -111,9 +147,17 @@ github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= +github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc= +github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= +github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ= +github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng= +github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= @@ -122,7 +166,9 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= @@ -146,11 +192,17 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= +golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= +golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= +golang.org/x/crypto v0.16.0 h1:mMMrFzRSCF0GvB7Ne27XVtVAaXLrPmgPC7/v0tkwHaY= +golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= golang.org/x/exp v0.0.0-20231219180239-dc181d75b848 h1:+iq7lrkxmFNBM7xx+Rae2W6uyPfhPeDWD+n+JgppptE= golang.org/x/exp v0.0.0-20231219180239-dc181d75b848/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -159,6 +211,9 @@ golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= golang.org/x/oauth2 v0.15.0 h1:s8pnnxNVzjWyrvYdFUQq5llS1PX2zhPXmccZv99h7uQ= @@ -167,6 +222,7 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE= golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -175,19 +231,29 @@ golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4= golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= +golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= @@ -197,6 +263,7 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.16.1 h1:TLyB3WofjdOEepBHAU20JdNC1Zbg87elYofWYAY5oZA= golang.org/x/tools v0.16.1/go.mod h1:kYVVN6I1mBNoB1OX+noeBjbRk4IUEPa7JJ+TJMEooJ0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -218,9 +285,13 @@ gopkg.in/evanphx/json-patch.v5 v5.7.0 h1:dGKGylPlZ/jus2g1YqhhyzfH0gPy2R8/MYUpW/O gopkg.in/evanphx/json-patch.v5 v5.7.0/go.mod h1:/kvTRh1TVm5wuM6OkHxqXtE/1nUZZpihg29RtuIyfvk= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go index 5fb29dd..4e8ffbc 100644 --- a/pkg/utils/utils.go +++ b/pkg/utils/utils.go @@ -2,6 +2,7 @@ package utils import ( "errors" + "strings" "github.com/golang-jwt/jwt/v5" ) @@ -16,3 +17,30 @@ func ParseJWTWithoutVerify(token string, options ...jwt.ParserOption) (*jwt.Toke } return t, err } + +// SplitPublicKeyBlocks splits a string containing multiple PGP public key blocks into a slice of strings. +// Returns an error and the already found blocks if a block start is found without a matching block end. +func SplitPublicKeyBlocks(in string) ([]string, error) { + var blocks []string + const ( + BlockStart = "-----BEGIN PGP PUBLIC KEY BLOCK-----" + BlockEnd = "-----END PGP PUBLIC KEY BLOCK-----" + ) + + for { + start := strings.Index(in, BlockStart) + if start == -1 { + break + } + end := strings.Index(in, BlockEnd) + if end == -1 { + return blocks, errors.New("unmatched PGP public key block start") + } + end += len(BlockEnd) + + blocks = append(blocks, in[start:end]) + in = in[end:] + } + + return blocks, nil +} diff --git a/pkg/utils/utils_test.go b/pkg/utils/utils_test.go index 67012f4..9abc247 100644 --- a/pkg/utils/utils_test.go +++ b/pkg/utils/utils_test.go @@ -15,3 +15,50 @@ func Test_ParseJWTWithoutVerify(t *testing.T) { _, err = utils.ParseJWTWithoutVerify(v) require.NoError(t, err) } + +func Test_SplitPublicKeyBlocks(t *testing.T) { + input := ` +leading garbage +-----BEGIN PGP PUBLIC KEY BLOCK----- +Public Key Block 1 +-----END PGP PUBLIC KEY BLOCK----- +Some other content +-----BEGIN PGP PUBLIC KEY BLOCK----- +Public Key Block 2 +-----END PGP PUBLIC KEY BLOCK-----` + + expected := []string{ + `-----BEGIN PGP PUBLIC KEY BLOCK----- +Public Key Block 1 +-----END PGP PUBLIC KEY BLOCK-----`, + `-----BEGIN PGP PUBLIC KEY BLOCK----- +Public Key Block 2 +-----END PGP PUBLIC KEY BLOCK-----`, + } + + result, err := utils.SplitPublicKeyBlocks(input) + require.NoError(t, err) + require.Equal(t, expected, result) +} + +func Test_SplitPublicKeyBlocks_UnmatchedBegin(t *testing.T) { + input := ` +leading garbage +-----BEGIN PGP PUBLIC KEY BLOCK----- +Public Key Block 1 +-----END PGP PUBLIC KEY BLOCK----- +Some other content +-----BEGIN PGP PUBLIC KEY BLOCK----- +asdasd +` + + expected := []string{ + `-----BEGIN PGP PUBLIC KEY BLOCK----- +Public Key Block 1 +-----END PGP PUBLIC KEY BLOCK-----`, + } + + result, err := utils.SplitPublicKeyBlocks(input) + require.Error(t, err) + require.Equal(t, expected, result) +}