Skip to content

Commit

Permalink
Add support for STS endpoint in the Bucket API
Browse files Browse the repository at this point in the history
Signed-off-by: Matheus Pimenta <[email protected]>
  • Loading branch information
matheuscscp committed Jul 20, 2024
1 parent 58b4e6d commit 2d62c48
Show file tree
Hide file tree
Showing 7 changed files with 270 additions and 40 deletions.
8 changes: 8 additions & 0 deletions api/v1beta2/bucket_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,14 @@ type BucketSpec struct {
// +required
Endpoint string `json:"endpoint"`

// STSEndpoint is the HTTP/S endpoint of the Security Token Service from
// where temporary credentials will automatically be fetched in the
// absence of a Secret reference.
//
// This field is only supported for the `generic` and `aws` providers.
// +optional
STSEndpoint string `json:"stsEndpoint,omitempty"`

// Insecure allows connecting to a non-TLS HTTP Endpoint.
// +optional
Insecure bool `json:"insecure,omitempty"`
Expand Down
9 changes: 9 additions & 0 deletions config/crd/bases/source.toolkit.fluxcd.io_buckets.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -420,6 +420,15 @@ spec:
required:
- name
type: object
stsEndpoint:
description: |-
STSEndpoint is the HTTP/S endpoint of the Security Token Service from
where temporary credentials will automatically be fetched in the
absence of a Secret reference.
This field is only supported for the `generic` and `aws` providers.
type: string
suspend:
description: |-
Suspend tells the controller to suspend the reconciliation of this
Expand Down
30 changes: 30 additions & 0 deletions docs/api/v1beta2/source.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,21 @@ string
</tr>
<tr>
<td>
<code>stsEndpoint</code><br>
<em>
string
</em>
</td>
<td>
<em>(Optional)</em>
<p>STSEndpoint is the HTTP/S endpoint of the Security Token Service from
where temporary credentials will automatically be fetched in the
absence of a Secret reference.</p>
<p>This field is only supported for the <code>generic</code> and <code>aws</code> providers.</p>
</td>
</tr>
<tr>
<td>
<code>insecure</code><br>
<em>
bool
Expand Down Expand Up @@ -1480,6 +1495,21 @@ string
</tr>
<tr>
<td>
<code>stsEndpoint</code><br>
<em>
string
</em>
</td>
<td>
<em>(Optional)</em>
<p>STSEndpoint is the HTTP/S endpoint of the Security Token Service from
where temporary credentials will automatically be fetched in the
absence of a Secret reference.</p>
<p>This field is only supported for the <code>generic</code> and <code>aws</code> providers.</p>
</td>
</tr>
<tr>
<td>
<code>insecure</code><br>
<em>
bool
Expand Down
8 changes: 8 additions & 0 deletions docs/spec/v1beta2/buckets.md
Original file line number Diff line number Diff line change
Expand Up @@ -749,6 +749,14 @@ 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 Endpoint

`.spec.stsEndpoint` is an optional field that specifies the HTTP/S endpoint
of the Security Token Service from where temporary credentials will automatically
be fetched in the absence of a Secret reference.

This field is only supported for the `generic` and `aws` providers.

### Bucket name

`.spec.bucketName` is a required field that specifies which object storage
Expand Down
41 changes: 26 additions & 15 deletions pkg/minio/minio.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,21 +88,6 @@ 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("")
}

var transportOpts []func(*http.Transport)

if minioOpts.Secure && tlsConfig != nil {
Expand All @@ -117,6 +102,32 @@ func NewClient(bucket *sourcev1.Bucket, opts ...Option) (*MinioClient, error) {
})
}

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 || bucket.Spec.STSEndpoint != "" {
creds := credentials.NewIAM(bucket.Spec.STSEndpoint)
if len(transportOpts) > 0 {
transport := http.DefaultTransport.(*http.Transport).Clone()
for _, opt := range transportOpts {
opt(transport)
}
creds = credentials.New(&credentials.IAM{
Client: &http.Client{Transport: transport},
Endpoint: bucket.Spec.STSEndpoint,
})
}
minioOpts.Creds = creds
}

if len(transportOpts) > 0 {
transport, err := minio.DefaultTransport(minioOpts.Secure)
if err != nil {
Expand Down
161 changes: 136 additions & 25 deletions pkg/minio/minio_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"context"
"crypto/tls"
"crypto/x509"
"encoding/json"
"errors"
"fmt"
"log"
Expand All @@ -28,13 +29,14 @@ import (
"net/url"
"os"
"path/filepath"
"strconv"
"strings"
"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"
Expand All @@ -45,6 +47,7 @@ import (
"github.com/fluxcd/pkg/sourceignore"

sourcev1 "github.com/fluxcd/source-controller/api/v1beta2"
testproxy "github.com/fluxcd/source-controller/tests/proxy"
)

const (
Expand Down Expand Up @@ -244,34 +247,142 @@ func TestFGetObject(t *testing.T) {
assert.NilError(t, err)
}

func TestNewClientAndFGetObjectWithProxy(t *testing.T) {
func TestNewClientAndFGetObjectWithSTSEndpoint(t *testing.T) {
// start a mock STS server
stsListener, err := net.Listen("tcp", ":0")
assert.NilError(t, err, "could not start STS listener")
defer stsListener.Close()
stsAddr := stsListener.Addr().String()
stsEndpoint := fmt.Sprintf("http://%s", stsAddr)
stsAddrParts := strings.Split(stsAddr, ":")
stsPortStr := stsAddrParts[len(stsAddrParts)-1]
stsPort, err := strconv.Atoi(stsPortStr)
assert.NilError(t, err)
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
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,
proxyAddr, proxyPort := testproxy.New(t)

tests := []struct {
name string
stsEndpoint string
opts []Option
errSubstring string
}{
{
name: "with correct endpoint",
stsEndpoint: stsEndpoint,
},
{
name: "with incorrect endpoint",
stsEndpoint: fmt.Sprintf("http://localhost:%d", stsPort+1),
errSubstring: "connection refused",
},
{
name: "with correct endpoint and proxy",
stsEndpoint: stsEndpoint,
opts: []Option{WithProxyURL(&url.URL{Scheme: "http", Host: proxyAddr})},
},
{
name: "with correct endpoint and incorrect proxy",
stsEndpoint: stsEndpoint,
opts: []Option{WithProxyURL(&url.URL{Scheme: "http", Host: fmt.Sprintf("localhost:%d", proxyPort+1)})},
errSubstring: "connection refused",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
roleCredsRetrieved = false
bucket := bucketStub(bucket, testMinioAddress)
bucket.Spec.STSEndpoint = tt.stsEndpoint
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.errSubstring != "" {
assert.ErrorContains(t, err, tt.errSubstring)
} 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) {
Expand Down
53 changes: 53 additions & 0 deletions tests/proxy/proxy.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*
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"
"net/http"
"strconv"
"strings"
"testing"

"github.com/elazarl/goproxy"
"gotest.tools/assert"
)

// 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) {
lis, err := net.Listen("tcp", ":0")
assert.NilError(t, err, "could not start proxy server")
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)

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
}

0 comments on commit 2d62c48

Please sign in to comment.