Add support for AWS STS endpoint in the Bucket API
Signed-off-by: Matheus Pimenta <matheuscscp@gmail.com>
This commit is contained in:
parent
218af573a3
commit
7536ab4b02
|
@ -108,4 +108,7 @@ const (
|
|||
// PatchOperationFailedReason signals a failure in patching a kubernetes API
|
||||
// object.
|
||||
PatchOperationFailedReason string = "PatchOperationFailed"
|
||||
|
||||
// InvalidSTSConfigurationReason signals that the STS configurtion is invalid.
|
||||
InvalidSTSConfigurationReason string = "InvalidSTSConfiguration"
|
||||
)
|
||||
|
|
|
@ -49,6 +49,8 @@ const (
|
|||
|
||||
// BucketSpec specifies the required configuration to produce an Artifact for
|
||||
// an object storage bucket.
|
||||
// +kubebuilder:validation:XValidation:rule="self.provider == 'aws' || !has(self.sts)", message="STS configuration is only supported for the 'aws' Bucket provider"
|
||||
// +kubebuilder:validation:XValidation:rule="self.provider != 'aws' || !has(self.sts) || self.sts.provider == 'aws'", message="'aws' is the only supported STS provider for the 'aws' Bucket provider"
|
||||
type BucketSpec struct {
|
||||
// Provider of the object storage bucket.
|
||||
// Defaults to 'generic', which expects an S3 (API) compatible object
|
||||
|
@ -66,6 +68,14 @@ type BucketSpec struct {
|
|||
// +required
|
||||
Endpoint string `json:"endpoint"`
|
||||
|
||||
// STS specifies the required configuration to use a Security Token
|
||||
// Service for fetching temporary credentials to authenticate in a
|
||||
// Bucket provider.
|
||||
//
|
||||
// This field is only supported for the `aws` provider.
|
||||
// +optional
|
||||
STS *BucketSTSSpec `json:"sts,omitempty"`
|
||||
|
||||
// Insecure allows connecting to a non-TLS HTTP Endpoint.
|
||||
// +optional
|
||||
Insecure bool `json:"insecure,omitempty"`
|
||||
|
@ -140,6 +150,22 @@ type BucketSpec struct {
|
|||
AccessFrom *acl.AccessFrom `json:"accessFrom,omitempty"`
|
||||
}
|
||||
|
||||
// BucketSTSSpec specifies the required configuration to use a Security Token
|
||||
// Service for fetching temporary credentials to authenticate in a Bucket
|
||||
// provider.
|
||||
type BucketSTSSpec struct {
|
||||
// Provider of the Security Token Service.
|
||||
// +kubebuilder:validation:Enum=aws
|
||||
// +required
|
||||
Provider string `json:"provider"`
|
||||
|
||||
// Endpoint is the HTTP/S endpoint of the Security Token Service from
|
||||
// where temporary credentials will be fetched.
|
||||
// +required
|
||||
// +kubebuilder:validation:Pattern="^(http|https)://.*$"
|
||||
Endpoint string `json:"endpoint"`
|
||||
}
|
||||
|
||||
// BucketStatus records the observed state of a Bucket.
|
||||
type BucketStatus struct {
|
||||
// ObservedGeneration is the last observed generation of the Bucket object.
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
Copyright 2024 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 v1beta2
|
||||
|
||||
const (
|
||||
// STSProviderAmazon represents the AWS provider for Security Token Service.
|
||||
// Provides support for fetching temporary credentials from an AWS STS endpoint.
|
||||
STSProviderAmazon string = "aws"
|
||||
)
|
|
@ -115,9 +115,29 @@ func (in *BucketList) DeepCopyObject() runtime.Object {
|
|||
return nil
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *BucketSTSSpec) DeepCopyInto(out *BucketSTSSpec) {
|
||||
*out = *in
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BucketSTSSpec.
|
||||
func (in *BucketSTSSpec) DeepCopy() *BucketSTSSpec {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(BucketSTSSpec)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *BucketSpec) DeepCopyInto(out *BucketSpec) {
|
||||
*out = *in
|
||||
if in.STS != nil {
|
||||
in, out := &in.STS, &out.STS
|
||||
*out = new(BucketSTSSpec)
|
||||
**out = **in
|
||||
}
|
||||
if in.SecretRef != nil {
|
||||
in, out := &in.SecretRef, &out.SecretRef
|
||||
*out = new(meta.LocalObjectReference)
|
||||
|
|
|
@ -420,6 +420,30 @@ spec:
|
|||
required:
|
||||
- name
|
||||
type: object
|
||||
sts:
|
||||
description: |-
|
||||
STS specifies the required configuration to use a Security Token
|
||||
Service for fetching temporary credentials to authenticate in a
|
||||
Bucket provider.
|
||||
|
||||
|
||||
This field is only supported for the `aws` provider.
|
||||
properties:
|
||||
endpoint:
|
||||
description: |-
|
||||
Endpoint is the HTTP/S endpoint of the Security Token Service from
|
||||
where temporary credentials will be fetched.
|
||||
pattern: ^(http|https)://.*$
|
||||
type: string
|
||||
provider:
|
||||
description: Provider of the Security Token Service.
|
||||
enum:
|
||||
- aws
|
||||
type: string
|
||||
required:
|
||||
- endpoint
|
||||
- provider
|
||||
type: object
|
||||
suspend:
|
||||
description: |-
|
||||
Suspend tells the controller to suspend the reconciliation of this
|
||||
|
@ -435,6 +459,13 @@ spec:
|
|||
- endpoint
|
||||
- interval
|
||||
type: object
|
||||
x-kubernetes-validations:
|
||||
- message: STS configuration is only supported for the 'aws' Bucket provider
|
||||
rule: self.provider == 'aws' || !has(self.sts)
|
||||
- message: '''aws'' is the only supported STS provider for the ''aws''
|
||||
Bucket provider'
|
||||
rule: self.provider != 'aws' || !has(self.sts) || self.sts.provider
|
||||
== 'aws'
|
||||
status:
|
||||
default:
|
||||
observedGeneration: -1
|
||||
|
|
|
@ -114,6 +114,23 @@ string
|
|||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<code>sts</code><br>
|
||||
<em>
|
||||
<a href="#source.toolkit.fluxcd.io/v1beta2.BucketSTSSpec">
|
||||
BucketSTSSpec
|
||||
</a>
|
||||
</em>
|
||||
</td>
|
||||
<td>
|
||||
<em>(Optional)</em>
|
||||
<p>STS specifies the required configuration to use a Security Token
|
||||
Service for fetching temporary credentials to authenticate in a
|
||||
Bucket provider.</p>
|
||||
<p>This field is only supported for the <code>aws</code> provider.</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<code>insecure</code><br>
|
||||
<em>
|
||||
bool
|
||||
|
@ -1424,6 +1441,52 @@ map[string]string
|
|||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<h3 id="source.toolkit.fluxcd.io/v1beta2.BucketSTSSpec">BucketSTSSpec
|
||||
</h3>
|
||||
<p>
|
||||
(<em>Appears on:</em>
|
||||
<a href="#source.toolkit.fluxcd.io/v1beta2.BucketSpec">BucketSpec</a>)
|
||||
</p>
|
||||
<p>BucketSTSSpec specifies the required configuration to use a Security Token
|
||||
Service for fetching temporary credentials to authenticate in a Bucket
|
||||
provider.</p>
|
||||
<div class="md-typeset__scrollwrap">
|
||||
<div class="md-typeset__table">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Field</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<code>provider</code><br>
|
||||
<em>
|
||||
string
|
||||
</em>
|
||||
</td>
|
||||
<td>
|
||||
<p>Provider of the Security Token Service.</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<code>endpoint</code><br>
|
||||
<em>
|
||||
string
|
||||
</em>
|
||||
</td>
|
||||
<td>
|
||||
<p>Endpoint is the HTTP/S endpoint of the Security Token Service from
|
||||
where temporary credentials will be fetched.</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<h3 id="source.toolkit.fluxcd.io/v1beta2.BucketSpec">BucketSpec
|
||||
</h3>
|
||||
<p>
|
||||
|
@ -1480,6 +1543,23 @@ string
|
|||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<code>sts</code><br>
|
||||
<em>
|
||||
<a href="#source.toolkit.fluxcd.io/v1beta2.BucketSTSSpec">
|
||||
BucketSTSSpec
|
||||
</a>
|
||||
</em>
|
||||
</td>
|
||||
<td>
|
||||
<em>(Optional)</em>
|
||||
<p>STS specifies the required configuration to use a Security Token
|
||||
Service for fetching temporary credentials to authenticate in a
|
||||
Bucket provider.</p>
|
||||
<p>This field is only supported for the <code>aws</code> provider.</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<code>insecure</code><br>
|
||||
<em>
|
||||
bool
|
||||
|
|
|
@ -749,6 +749,23 @@ HTTP endpoint requires enabling [`.spec.insecure`](#insecure).
|
|||
Some endpoints require the specification of a [`.spec.region`](#region),
|
||||
see [Provider](#provider) for more (provider specific) examples.
|
||||
|
||||
### STS
|
||||
|
||||
`.spec.sts` is an optional field for specifying the Security Token Service
|
||||
configuration. A Security Token Service (STS) is a web service that issues
|
||||
temporary security credentials. By adding this field, one may specify the
|
||||
STS endpoint from where temporary credentials will be fetched.
|
||||
|
||||
If using `.spec.sts`, the following fields are required:
|
||||
|
||||
- `.spec.sts.provider`, the Security Token Service provider. The only supported
|
||||
option is `aws`.
|
||||
- `.spec.sts.endpoint`, the HTTP/S endpoint of the Security Token Service. In
|
||||
the case of AWS, this can be `https://sts.amazonaws.com`, or a Regional STS
|
||||
Endpoint, or an Interface Endpoint created inside a VPC.
|
||||
|
||||
This field is only supported for the `aws` bucket provider.
|
||||
|
||||
### Bucket name
|
||||
|
||||
`.spec.bucketName` is a required field that specifies which object storage
|
||||
|
|
|
@ -463,6 +463,19 @@ func (r *BucketReconciler) reconcileSource(ctx context.Context, sp *patch.Serial
|
|||
conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, e.Reason, "%s", e)
|
||||
return sreconcile.ResultEmpty, e
|
||||
}
|
||||
if sts := obj.Spec.STS; sts != nil {
|
||||
if err := minio.ValidateSTSProvider(obj.Spec.Provider, sts.Provider); err != nil {
|
||||
e := serror.NewStalling(err, sourcev1.InvalidSTSConfigurationReason)
|
||||
conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, e.Reason, "%s", e)
|
||||
return sreconcile.ResultEmpty, e
|
||||
}
|
||||
if _, err := url.Parse(sts.Endpoint); err != nil {
|
||||
err := fmt.Errorf("failed to parse STS endpoint '%s': %w", sts.Endpoint, err)
|
||||
e := serror.NewStalling(err, sourcev1.URLInvalidReason)
|
||||
conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, e.Reason, "%s", e)
|
||||
return sreconcile.ResultEmpty, e
|
||||
}
|
||||
}
|
||||
tlsConfig, err := r.getTLSConfig(ctx, obj)
|
||||
if err != nil {
|
||||
e := serror.NewGeneric(err, sourcev1.AuthenticationFailedReason)
|
||||
|
|
|
@ -608,6 +608,45 @@ func TestBucketReconciler_reconcileSource_generic(t *testing.T) {
|
|||
*conditions.UnknownCondition(meta.ReadyCondition, "foo", "bar"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Observes incompatible STS provider",
|
||||
bucketName: "dummy",
|
||||
beforeFunc: func(obj *bucketv1.Bucket) {
|
||||
obj.Spec.Provider = "generic"
|
||||
obj.Spec.STS = &bucketv1.BucketSTSSpec{
|
||||
Provider: "aws",
|
||||
}
|
||||
conditions.MarkReconciling(obj, meta.ProgressingReason, "foo")
|
||||
conditions.MarkUnknown(obj, meta.ReadyCondition, "foo", "bar")
|
||||
},
|
||||
wantErr: true,
|
||||
assertIndex: index.NewDigester(),
|
||||
assertConditions: []metav1.Condition{
|
||||
*conditions.TrueCondition(sourcev1.FetchFailedCondition, sourcev1.InvalidSTSConfigurationReason, "STS configuration is not supported for 'generic' bucket provider"),
|
||||
*conditions.TrueCondition(meta.ReconcilingCondition, meta.ProgressingReason, "foo"),
|
||||
*conditions.UnknownCondition(meta.ReadyCondition, "foo", "bar"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Observes invalid STS endpoint",
|
||||
bucketName: "dummy",
|
||||
beforeFunc: func(obj *bucketv1.Bucket) {
|
||||
obj.Spec.Provider = "aws" // TODO: change to generic when ldap STS provider is implemented
|
||||
obj.Spec.STS = &bucketv1.BucketSTSSpec{
|
||||
Provider: "aws", // TODO: change to ldap when ldap STS provider is implemented
|
||||
Endpoint: "something\t",
|
||||
}
|
||||
conditions.MarkReconciling(obj, meta.ProgressingReason, "foo")
|
||||
conditions.MarkUnknown(obj, meta.ReadyCondition, "foo", "bar")
|
||||
},
|
||||
wantErr: true,
|
||||
assertIndex: index.NewDigester(),
|
||||
assertConditions: []metav1.Condition{
|
||||
*conditions.TrueCondition(sourcev1.FetchFailedCondition, sourcev1.URLInvalidReason, "failed to parse STS endpoint 'something\t': parse \"something\\t\": net/url: invalid control character in URL"),
|
||||
*conditions.TrueCondition(meta.ReconcilingCondition, meta.ProgressingReason, "foo"),
|
||||
*conditions.UnknownCondition(meta.ReadyCondition, "foo", "bar"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Transient bucket name API failure",
|
||||
beforeFunc: func(obj *bucketv1.Bucket) {
|
||||
|
@ -1762,3 +1801,119 @@ func TestBucketReconciler_getProxyURL(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBucketReconciler_APIServerValidation_STS(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
bucketProvider string
|
||||
stsConfig *bucketv1.BucketSTSSpec
|
||||
err string
|
||||
}{
|
||||
{
|
||||
name: "gcp unsupported",
|
||||
bucketProvider: "gcp",
|
||||
stsConfig: &bucketv1.BucketSTSSpec{
|
||||
Provider: "aws",
|
||||
Endpoint: "http://test",
|
||||
},
|
||||
err: "STS configuration is only supported for the 'aws' Bucket provider",
|
||||
},
|
||||
{
|
||||
name: "azure unsupported",
|
||||
bucketProvider: "azure",
|
||||
stsConfig: &bucketv1.BucketSTSSpec{
|
||||
Provider: "aws",
|
||||
Endpoint: "http://test",
|
||||
},
|
||||
err: "STS configuration is only supported for the 'aws' Bucket provider",
|
||||
},
|
||||
{
|
||||
name: "generic unsupported",
|
||||
bucketProvider: "generic",
|
||||
stsConfig: &bucketv1.BucketSTSSpec{
|
||||
Provider: "aws",
|
||||
Endpoint: "http://test",
|
||||
},
|
||||
err: "STS configuration is only supported for the 'aws' Bucket provider",
|
||||
},
|
||||
{
|
||||
name: "aws supported",
|
||||
bucketProvider: "aws",
|
||||
stsConfig: &bucketv1.BucketSTSSpec{
|
||||
Provider: "aws",
|
||||
Endpoint: "http://test",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "invalid endpoint",
|
||||
bucketProvider: "aws",
|
||||
stsConfig: &bucketv1.BucketSTSSpec{
|
||||
Provider: "aws",
|
||||
Endpoint: "test",
|
||||
},
|
||||
err: "spec.sts.endpoint in body should match '^(http|https)://.*$'",
|
||||
},
|
||||
{
|
||||
name: "gcp can be created without STS config",
|
||||
bucketProvider: "gcp",
|
||||
},
|
||||
{
|
||||
name: "azure can be created without STS config",
|
||||
bucketProvider: "azure",
|
||||
},
|
||||
{
|
||||
name: "generic can be created without STS config",
|
||||
bucketProvider: "generic",
|
||||
},
|
||||
{
|
||||
name: "aws can be created without STS config",
|
||||
bucketProvider: "aws",
|
||||
},
|
||||
// Can't be tested at present with only one allowed sts provider.
|
||||
// {
|
||||
// name: "ldap unsupported for aws",
|
||||
// bucketProvider: "aws",
|
||||
// stsConfig: &bucketv1.BucketSTSSpec{
|
||||
// Provider: "ldap",
|
||||
// Endpoint: "http://test",
|
||||
// },
|
||||
// err: "'aws' is the only supported STS provider for the 'aws' Bucket provider",
|
||||
// },
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
|
||||
obj := &bucketv1.Bucket{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
GenerateName: "bucket-reconcile-",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: bucketv1.BucketSpec{
|
||||
Provider: tt.bucketProvider,
|
||||
BucketName: "test",
|
||||
Endpoint: "test",
|
||||
Suspend: true,
|
||||
Interval: metav1.Duration{Duration: interval},
|
||||
Timeout: &metav1.Duration{Duration: timeout},
|
||||
STS: tt.stsConfig,
|
||||
},
|
||||
}
|
||||
|
||||
err := testEnv.Create(ctx, obj)
|
||||
if err == nil {
|
||||
defer func() {
|
||||
err := testEnv.Delete(ctx, obj)
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
}()
|
||||
}
|
||||
|
||||
if tt.err != "" {
|
||||
g.Expect(err.Error()).To(ContainSubstring(tt.err))
|
||||
} else {
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -71,14 +71,10 @@ func WithProxyURL(proxyURL *url.URL) Option {
|
|||
|
||||
// NewClient creates a new Minio storage client.
|
||||
func NewClient(bucket *sourcev1.Bucket, opts ...Option) (*MinioClient, error) {
|
||||
|
||||
var o options
|
||||
for _, opt := range opts {
|
||||
opt(&o)
|
||||
}
|
||||
secret := o.secret
|
||||
tlsConfig := o.tlsConfig
|
||||
proxyURL := o.proxyURL
|
||||
|
||||
minioOpts := minio.Options{
|
||||
Region: bucket.Spec.Region,
|
||||
|
@ -88,32 +84,24 @@ func NewClient(bucket *sourcev1.Bucket, opts ...Option) (*MinioClient, error) {
|
|||
// auto access, which we believe can cover most use cases.
|
||||
}
|
||||
|
||||
if secret != nil {
|
||||
var accessKey, secretKey string
|
||||
if k, ok := secret.Data["accesskey"]; ok {
|
||||
accessKey = string(k)
|
||||
}
|
||||
if k, ok := secret.Data["secretkey"]; ok {
|
||||
secretKey = string(k)
|
||||
}
|
||||
if accessKey != "" && secretKey != "" {
|
||||
minioOpts.Creds = credentials.NewStaticV4(accessKey, secretKey, "")
|
||||
}
|
||||
} else if bucket.Spec.Provider == sourcev1.AmazonBucketProvider {
|
||||
minioOpts.Creds = credentials.NewIAM("")
|
||||
switch bucketProvider := bucket.Spec.Provider; {
|
||||
case o.secret != nil:
|
||||
minioOpts.Creds = newCredsFromSecret(o.secret)
|
||||
case bucketProvider == sourcev1.AmazonBucketProvider:
|
||||
minioOpts.Creds = newAWSCreds(bucket, o.proxyURL)
|
||||
}
|
||||
|
||||
var transportOpts []func(*http.Transport)
|
||||
|
||||
if minioOpts.Secure && tlsConfig != nil {
|
||||
if minioOpts.Secure && o.tlsConfig != nil {
|
||||
transportOpts = append(transportOpts, func(t *http.Transport) {
|
||||
t.TLSClientConfig = tlsConfig.Clone()
|
||||
t.TLSClientConfig = o.tlsConfig.Clone()
|
||||
})
|
||||
}
|
||||
|
||||
if proxyURL != nil {
|
||||
if o.proxyURL != nil {
|
||||
transportOpts = append(transportOpts, func(t *http.Transport) {
|
||||
t.Proxy = http.ProxyURL(proxyURL)
|
||||
t.Proxy = http.ProxyURL(o.proxyURL)
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -135,6 +123,42 @@ func NewClient(bucket *sourcev1.Bucket, opts ...Option) (*MinioClient, error) {
|
|||
return &MinioClient{Client: client}, nil
|
||||
}
|
||||
|
||||
// newCredsFromSecret creates a new Minio credentials object from the provided
|
||||
// secret.
|
||||
func newCredsFromSecret(secret *corev1.Secret) *credentials.Credentials {
|
||||
var accessKey, secretKey string
|
||||
if k, ok := secret.Data["accesskey"]; ok {
|
||||
accessKey = string(k)
|
||||
}
|
||||
if k, ok := secret.Data["secretkey"]; ok {
|
||||
secretKey = string(k)
|
||||
}
|
||||
if accessKey != "" && secretKey != "" {
|
||||
return credentials.NewStaticV4(accessKey, secretKey, "")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// newAWSCreds creates a new Minio credentials object for `aws` bucket provider.
|
||||
func newAWSCreds(bucket *sourcev1.Bucket, proxyURL *url.URL) *credentials.Credentials {
|
||||
stsEndpoint := ""
|
||||
if sts := bucket.Spec.STS; sts != nil {
|
||||
stsEndpoint = sts.Endpoint
|
||||
}
|
||||
|
||||
creds := credentials.NewIAM(stsEndpoint)
|
||||
if proxyURL != nil {
|
||||
transport := http.DefaultTransport.(*http.Transport).Clone()
|
||||
transport.Proxy = http.ProxyURL(proxyURL)
|
||||
client := &http.Client{Transport: transport}
|
||||
creds = credentials.New(&credentials.IAM{
|
||||
Client: client,
|
||||
Endpoint: stsEndpoint,
|
||||
})
|
||||
}
|
||||
return creds
|
||||
}
|
||||
|
||||
// ValidateSecret validates the credential secret. The provided Secret may
|
||||
// be nil.
|
||||
func ValidateSecret(secret *corev1.Secret) error {
|
||||
|
@ -151,6 +175,24 @@ func ValidateSecret(secret *corev1.Secret) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// ValidateSTSProvider validates the STS provider.
|
||||
func ValidateSTSProvider(bucketProvider, stsProvider string) error {
|
||||
errProviderIncompatbility := fmt.Errorf("STS provider '%s' is not supported for '%s' bucket provider",
|
||||
stsProvider, bucketProvider)
|
||||
|
||||
switch bucketProvider {
|
||||
case sourcev1.AmazonBucketProvider:
|
||||
switch stsProvider {
|
||||
case sourcev1.STSProviderAmazon:
|
||||
return nil
|
||||
default:
|
||||
return errProviderIncompatbility
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Errorf("STS configuration is not supported for '%s' bucket provider", bucketProvider)
|
||||
}
|
||||
|
||||
// FGetObject gets the object from the provided object storage bucket, and
|
||||
// writes it to targetPath.
|
||||
// It returns the etag of the successfully fetched file, or any error.
|
||||
|
|
|
@ -20,10 +20,10 @@ import (
|
|||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
|
@ -32,9 +32,9 @@ import (
|
|||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/elazarl/goproxy"
|
||||
"github.com/google/uuid"
|
||||
miniov7 "github.com/minio/minio-go/v7"
|
||||
"github.com/minio/minio-go/v7/pkg/credentials"
|
||||
"github.com/ory/dockertest/v3"
|
||||
"github.com/ory/dockertest/v3/docker"
|
||||
"gotest.tools/assert"
|
||||
|
@ -45,6 +45,8 @@ import (
|
|||
"github.com/fluxcd/pkg/sourceignore"
|
||||
|
||||
sourcev1 "github.com/fluxcd/source-controller/api/v1beta2"
|
||||
testlistener "github.com/fluxcd/source-controller/tests/listener"
|
||||
testproxy "github.com/fluxcd/source-controller/tests/proxy"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -244,34 +246,153 @@ func TestFGetObject(t *testing.T) {
|
|||
assert.NilError(t, err)
|
||||
}
|
||||
|
||||
func TestNewClientAndFGetObjectWithProxy(t *testing.T) {
|
||||
// start proxy
|
||||
proxyListener, err := net.Listen("tcp", ":0")
|
||||
assert.NilError(t, err, "could not start proxy server")
|
||||
defer proxyListener.Close()
|
||||
proxyAddr := proxyListener.Addr().String()
|
||||
proxyHandler := goproxy.NewProxyHttpServer()
|
||||
proxyHandler.Verbose = true
|
||||
proxyServer := &http.Server{
|
||||
Addr: proxyAddr,
|
||||
Handler: proxyHandler,
|
||||
func TestNewClientAndFGetObjectWithSTSEndpoint(t *testing.T) {
|
||||
// start a mock STS server
|
||||
stsListener, stsAddr, stsPort := testlistener.New(t)
|
||||
stsEndpoint := fmt.Sprintf("http://%s", stsAddr)
|
||||
stsHandler := http.NewServeMux()
|
||||
stsHandler.HandleFunc("PUT "+credentials.TokenPath,
|
||||
func(w http.ResponseWriter, r *http.Request) {
|
||||
_, err := w.Write([]byte("mock-token"))
|
||||
assert.NilError(t, err)
|
||||
})
|
||||
stsHandler.HandleFunc("GET "+credentials.DefaultIAMSecurityCredsPath,
|
||||
func(w http.ResponseWriter, r *http.Request) {
|
||||
token := r.Header.Get(credentials.TokenRequestHeader)
|
||||
assert.Equal(t, token, "mock-token")
|
||||
_, err := w.Write([]byte("mock-role"))
|
||||
assert.NilError(t, err)
|
||||
})
|
||||
var roleCredsRetrieved bool
|
||||
stsHandler.HandleFunc("GET "+credentials.DefaultIAMSecurityCredsPath+"mock-role",
|
||||
func(w http.ResponseWriter, r *http.Request) {
|
||||
token := r.Header.Get(credentials.TokenRequestHeader)
|
||||
assert.Equal(t, token, "mock-token")
|
||||
err := json.NewEncoder(w).Encode(map[string]any{
|
||||
"Code": "Success",
|
||||
"AccessKeyID": testMinioRootUser,
|
||||
"SecretAccessKey": testMinioRootPassword,
|
||||
})
|
||||
assert.NilError(t, err)
|
||||
roleCredsRetrieved = true
|
||||
})
|
||||
stsServer := &http.Server{
|
||||
Addr: stsAddr,
|
||||
Handler: stsHandler,
|
||||
}
|
||||
go stsServer.Serve(stsListener)
|
||||
defer stsServer.Shutdown(context.Background())
|
||||
|
||||
// start proxy
|
||||
proxyAddr, proxyPort := testproxy.New(t)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
provider string
|
||||
stsSpec *sourcev1.BucketSTSSpec
|
||||
opts []Option
|
||||
err string
|
||||
}{
|
||||
{
|
||||
name: "with correct endpoint",
|
||||
provider: "aws",
|
||||
stsSpec: &sourcev1.BucketSTSSpec{
|
||||
Provider: "aws",
|
||||
Endpoint: stsEndpoint,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "with incorrect endpoint",
|
||||
provider: "aws",
|
||||
stsSpec: &sourcev1.BucketSTSSpec{
|
||||
Provider: "aws",
|
||||
Endpoint: fmt.Sprintf("http://localhost:%d", stsPort+1),
|
||||
},
|
||||
err: "connection refused",
|
||||
},
|
||||
{
|
||||
name: "with correct endpoint and proxy",
|
||||
provider: "aws",
|
||||
stsSpec: &sourcev1.BucketSTSSpec{
|
||||
Provider: "aws",
|
||||
Endpoint: stsEndpoint,
|
||||
},
|
||||
opts: []Option{WithProxyURL(&url.URL{Scheme: "http", Host: proxyAddr})},
|
||||
},
|
||||
{
|
||||
name: "with correct endpoint and incorrect proxy",
|
||||
provider: "aws",
|
||||
stsSpec: &sourcev1.BucketSTSSpec{
|
||||
Provider: "aws",
|
||||
Endpoint: stsEndpoint,
|
||||
},
|
||||
opts: []Option{WithProxyURL(&url.URL{Scheme: "http", Host: fmt.Sprintf("localhost:%d", proxyPort+1)})},
|
||||
err: "connection refused",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
roleCredsRetrieved = false
|
||||
bucket := bucketStub(bucket, testMinioAddress)
|
||||
bucket.Spec.Provider = tt.provider
|
||||
bucket.Spec.STS = tt.stsSpec
|
||||
minioClient, err := NewClient(bucket, append(tt.opts, WithTLSConfig(testTLSConfig))...)
|
||||
assert.NilError(t, err)
|
||||
assert.Assert(t, minioClient != nil)
|
||||
ctx := context.Background()
|
||||
tempDir := t.TempDir()
|
||||
path := filepath.Join(tempDir, sourceignore.IgnoreFile)
|
||||
_, err = minioClient.FGetObject(ctx, bucketName, objectName, path)
|
||||
if tt.err != "" {
|
||||
assert.ErrorContains(t, err, tt.err)
|
||||
} else {
|
||||
assert.NilError(t, err)
|
||||
assert.Assert(t, roleCredsRetrieved)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewClientAndFGetObjectWithProxy(t *testing.T) {
|
||||
proxyAddr, proxyPort := testproxy.New(t)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
proxyURL *url.URL
|
||||
errSubstring string
|
||||
}{
|
||||
{
|
||||
name: "with correct proxy",
|
||||
proxyURL: &url.URL{Scheme: "http", Host: proxyAddr},
|
||||
},
|
||||
{
|
||||
name: "with incorrect proxy",
|
||||
proxyURL: &url.URL{Scheme: "http", Host: fmt.Sprintf("localhost:%d", proxyPort+1)},
|
||||
errSubstring: "connection refused",
|
||||
},
|
||||
}
|
||||
go proxyServer.Serve(proxyListener)
|
||||
defer proxyServer.Shutdown(context.Background())
|
||||
proxyURL := &url.URL{Scheme: "http", Host: proxyAddr}
|
||||
|
||||
// run test
|
||||
minioClient, err := NewClient(bucketStub(bucket, testMinioAddress),
|
||||
WithSecret(secret.DeepCopy()),
|
||||
WithTLSConfig(testTLSConfig),
|
||||
WithProxyURL(proxyURL))
|
||||
assert.NilError(t, err)
|
||||
assert.Assert(t, minioClient != nil)
|
||||
ctx := context.Background()
|
||||
tempDir := t.TempDir()
|
||||
path := filepath.Join(tempDir, sourceignore.IgnoreFile)
|
||||
_, err = minioClient.FGetObject(ctx, bucketName, objectName, path)
|
||||
assert.NilError(t, err)
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
minioClient, err := NewClient(bucketStub(bucket, testMinioAddress),
|
||||
WithSecret(secret.DeepCopy()),
|
||||
WithTLSConfig(testTLSConfig),
|
||||
WithProxyURL(tt.proxyURL))
|
||||
assert.NilError(t, err)
|
||||
assert.Assert(t, minioClient != nil)
|
||||
ctx := context.Background()
|
||||
tempDir := t.TempDir()
|
||||
path := filepath.Join(tempDir, sourceignore.IgnoreFile)
|
||||
_, err = minioClient.FGetObject(ctx, bucketName, objectName, path)
|
||||
if tt.errSubstring != "" {
|
||||
assert.ErrorContains(t, err, tt.errSubstring)
|
||||
} else {
|
||||
assert.NilError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFGetObjectNotExists(t *testing.T) {
|
||||
|
@ -349,6 +470,47 @@ func TestValidateSecret(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestValidateSTSProvider(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
bucketProvider string
|
||||
stsProvider string
|
||||
err string
|
||||
}{
|
||||
{
|
||||
name: "aws",
|
||||
bucketProvider: "aws",
|
||||
stsProvider: "aws",
|
||||
},
|
||||
{
|
||||
name: "unsupported for aws",
|
||||
bucketProvider: "aws",
|
||||
stsProvider: "ldap",
|
||||
err: "STS provider 'ldap' is not supported for 'aws' bucket provider",
|
||||
},
|
||||
{
|
||||
name: "unsupported bucket provider",
|
||||
bucketProvider: "gcp",
|
||||
stsProvider: "gcp",
|
||||
err: "STS configuration is not supported for 'gcp' bucket provider",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
err := ValidateSTSProvider(tt.bucketProvider, tt.stsProvider)
|
||||
if tt.err != "" {
|
||||
assert.Error(t, err, tt.err)
|
||||
} else {
|
||||
assert.NilError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func bucketStub(bucket sourcev1.Bucket, endpoint string) *sourcev1.Bucket {
|
||||
b := bucket.DeepCopy()
|
||||
b.Spec.Endpoint = endpoint
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
Copyright 2024 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 testlistener
|
||||
|
||||
import (
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"gotest.tools/assert"
|
||||
)
|
||||
|
||||
// New creates a TCP listener on a random port and returns
|
||||
// the listener, the address and the port of this listener.
|
||||
// It also registers a cleanup function to close the listener
|
||||
// when the test ends.
|
||||
func New(t *testing.T) (net.Listener, string, int) {
|
||||
t.Helper()
|
||||
|
||||
lis, err := net.Listen("tcp", ":0")
|
||||
assert.NilError(t, err)
|
||||
t.Cleanup(func() { lis.Close() })
|
||||
|
||||
addr := lis.Addr().String()
|
||||
addrParts := strings.Split(addr, ":")
|
||||
portStr := addrParts[len(addrParts)-1]
|
||||
port, err := strconv.Atoi(portStr)
|
||||
assert.NilError(t, err)
|
||||
|
||||
return lis, addr, port
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
Copyright 2024 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 testproxy
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/elazarl/goproxy"
|
||||
|
||||
testlistener "github.com/fluxcd/source-controller/tests/listener"
|
||||
)
|
||||
|
||||
// New creates a new goproxy server on a random port and returns
|
||||
// the address and the port of this server. It also registers a
|
||||
// cleanup functions to close the server and the listener when
|
||||
// the test ends.
|
||||
func New(t *testing.T) (string, int) {
|
||||
t.Helper()
|
||||
|
||||
lis, addr, port := testlistener.New(t)
|
||||
|
||||
handler := goproxy.NewProxyHttpServer()
|
||||
handler.Verbose = true
|
||||
|
||||
server := &http.Server{
|
||||
Addr: addr,
|
||||
Handler: handler,
|
||||
}
|
||||
go server.Serve(lis)
|
||||
t.Cleanup(func() { server.Close() })
|
||||
|
||||
return addr, port
|
||||
}
|
Loading…
Reference in New Issue