Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add GCP storage authentication #434

Merged
merged 56 commits into from
Oct 14, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
7da9619
Feature: Add Support for Google Cloud Storage along with Workload Ide…
pa250194 Sep 1, 2021
a5588fb
Added Comments for reconcileWithGCP and reconcileWithMinio
pa250194 Sep 1, 2021
78379dd
Added initial testing for new GCP provider
pa250194 Sep 2, 2021
90395f4
Remove .DS_STORE file
pa250194 Sep 2, 2021
0444c6e
Service Account Key Authentication to GCP Provider
pa250194 Sep 10, 2021
eeb38bd
Tests for GCP Bucket Provider
pa250194 Sep 14, 2021
c204f6a
Added Tests to GCP provider
pa250194 Sep 15, 2021
6ff5970
Added more tests and cleaned up GCP provider logic
pa250194 Sep 16, 2021
fa8c4ca
Fix nil pointer dereference
pa250194 Sep 16, 2021
a6be9c8
Updated docs to include GCP provider instructions
pa250194 Sep 16, 2021
0b97151
Revert change to doc/api/source.md
pa250194 Sep 16, 2021
057c65e
Removed resumable downloads
pa250194 Sep 23, 2021
38be5ed
Cleanup obsolete comments
pa250194 Sep 23, 2021
7c0d4c0
Refactor comments and method names
pa250194 Sep 23, 2021
ad65ddd
Merge branch 'main' into gcp-bucket-provider
pa250194 Oct 8, 2021
911ecc6
Update go.sum
pa250194 Oct 11, 2021
69fffa0
Fixed spelling and capitalization
pa250194 Oct 12, 2021
572eed7
Add Support for GCP storage with workload identity
pa250194 Sep 1, 2021
a600528
Added Comments for reconcileWithGCP and reconcileWithMinio
pa250194 Sep 1, 2021
2cc48fe
Added initial testing for new GCP provider
pa250194 Sep 2, 2021
57b54c8
Service Account Key Authentication to GCP Provider
pa250194 Sep 10, 2021
1fae4f6
Tests for GCP Bucket Provider
pa250194 Sep 14, 2021
a46b0f5
Added Tests to GCP provider
pa250194 Sep 15, 2021
b02a762
Added more tests and cleaned up GCP provider logic
pa250194 Sep 16, 2021
57ef719
Updated docs to include GCP provider instructions
pa250194 Sep 16, 2021
02102de
Removed resumable downloads
pa250194 Sep 23, 2021
751243c
Refactor comments and method names
pa250194 Sep 23, 2021
116906c
Fixed spelling and capitalization
pa250194 Oct 12, 2021
0c0a76d
Merge branch 'gcp-bucket-provider' of https://github.com/pa250194/sou…
pa250194 Oct 14, 2021
f62571b
Added log for GCP provider auth error
pa250194 Oct 14, 2021
f797fbf
Added Logger to closing GCP client
pa250194 Oct 14, 2021
869c796
Update github.com/libgit2/git2go to v31.6.1
hiddeco Sep 10, 2021
c9e3f97
Add `docker-buildx` target to `Makefile`
hiddeco Sep 27, 2021
b283e3e
Change image to image under Flux organization
hiddeco Sep 30, 2021
500d0ae
Update base image to version with Darwin detection
hiddeco Oct 1, 2021
1b11e11
Allow libgit2 build to be enforced
hiddeco Oct 4, 2021
cc01df2
Detect macOS produced libgit2.dylib on Darwin
hiddeco Oct 4, 2021
153b122
Document libgit2 build behavior in CONTRIBUTING.md
hiddeco Oct 4, 2021
d04c532
Switch to scratch based libgit2 container image
hiddeco Oct 7, 2021
6101319
Update Dockerfile used in tests as well
hiddeco Oct 7, 2021
66fffe1
CONTRIBUTING: include pkg-config as macOS dep
hiddeco Oct 8, 2021
6fe6f07
Update containerd and runc to fix CVEs
stefanprodan Oct 8, 2021
5e6abae
Add ReconcileStrategy to HelmChart
arbourd Mar 13, 2021
96ab646
Release v0.16.0
hiddeco Oct 8, 2021
c2495ae
Fix generation of API documentation
hiddeco Oct 8, 2021
e2548cb
Update fluxcd/golang-with-libgit2 to 1.1.1-1
hiddeco Oct 8, 2021
38bf4d9
Fixed spelling and capitalization
pa250194 Oct 12, 2021
39811ed
Add Support for GCP storage with workload identity
pa250194 Sep 1, 2021
2baa8a2
Added Comments for reconcileWithGCP and reconcileWithMinio
pa250194 Sep 1, 2021
be1ed50
Service Account Key Authentication to GCP Provider
pa250194 Sep 10, 2021
99c79bf
Tests for GCP Bucket Provider
pa250194 Sep 14, 2021
c981305
Added Tests to GCP provider
pa250194 Sep 15, 2021
5077c1f
Added more tests and cleaned up GCP provider logic
pa250194 Sep 16, 2021
7921caf
Updated docs to include GCP provider instructions
pa250194 Sep 16, 2021
c4e4b39
Added Logger to closing GCP client
pa250194 Oct 14, 2021
8f0ea2e
Merge branch 'gcp-bucket-provider' of https://github.com/pa250194/sou…
pa250194 Oct 14, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion api/v1beta1/bucket_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ const (
// BucketSpec defines the desired state of an S3 compatible bucket
type BucketSpec struct {
// The S3 compatible storage provider name, default ('generic').
// +kubebuilder:validation:Enum=generic;aws
// +kubebuilder:validation:Enum=generic;aws;gcp
// +kubebuilder:default:=generic
// +optional
Provider string `json:"provider,omitempty"`
Expand Down Expand Up @@ -79,6 +79,7 @@ type BucketSpec struct {
const (
GenericBucketProvider string = "generic"
AmazonBucketProvider string = "aws"
GoogleBucketProvider string = "gcp"
)

// BucketStatus defines the observed state of a bucket
Expand Down
1 change: 1 addition & 0 deletions config/crd/bases/source.toolkit.fluxcd.io_buckets.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ spec:
enum:
- generic
- aws
- gcp
type: string
region:
description: The bucket region.
Expand Down
244 changes: 182 additions & 62 deletions controllers/bucket_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import (
"github.com/minio/minio-go/v7"
"github.com/minio/minio-go/v7/pkg/credentials"
"github.com/minio/minio-go/v7/pkg/s3utils"
"google.golang.org/api/option"
corev1 "k8s.io/api/core/v1"
apimeta "k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
Expand All @@ -46,6 +47,7 @@ import (
"github.com/fluxcd/pkg/runtime/events"
"github.com/fluxcd/pkg/runtime/metrics"
"github.com/fluxcd/pkg/runtime/predicates"
"github.com/fluxcd/source-controller/pkg/gcp"

sourcev1 "github.com/fluxcd/source-controller/api/v1beta1"
"github.com/fluxcd/source-controller/pkg/sourceignore"
Expand Down Expand Up @@ -176,77 +178,25 @@ func (r *BucketReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr
}

func (r *BucketReconciler) reconcile(ctx context.Context, bucket sourcev1.Bucket) (sourcev1.Bucket, error) {
s3Client, err := r.auth(ctx, bucket)
if err != nil {
err = fmt.Errorf("auth error: %w", err)
return sourcev1.BucketNotReady(bucket, sourcev1.AuthenticationFailedReason, err.Error()), err
}

// create tmp dir
var err error
var sourceBucket sourcev1.Bucket
tempDir, err := os.MkdirTemp("", bucket.Name)
if err != nil {
err = fmt.Errorf("tmp dir error: %w", err)
return sourcev1.BucketNotReady(bucket, sourcev1.StorageOperationFailedReason, err.Error()), err
}
defer os.RemoveAll(tempDir)

ctxTimeout, cancel := context.WithTimeout(ctx, bucket.Spec.Timeout.Duration)
defer cancel()

exists, err := s3Client.BucketExists(ctxTimeout, bucket.Spec.BucketName)
if err != nil {
return sourcev1.BucketNotReady(bucket, sourcev1.BucketOperationFailedReason, err.Error()), err
}
if !exists {
err = fmt.Errorf("bucket '%s' not found", bucket.Spec.BucketName)
return sourcev1.BucketNotReady(bucket, sourcev1.BucketOperationFailedReason, err.Error()), err
}

// Look for file with ignore rules first
// NB: S3 has flat filepath keys making it impossible to look
// for files in "subdirectories" without building up a tree first.
path := filepath.Join(tempDir, sourceignore.IgnoreFile)
if err := s3Client.FGetObject(ctxTimeout, bucket.Spec.BucketName, sourceignore.IgnoreFile, path, minio.GetObjectOptions{}); err != nil {
if resp, ok := err.(minio.ErrorResponse); ok && resp.Code != "NoSuchKey" {
return sourcev1.BucketNotReady(bucket, sourcev1.BucketOperationFailedReason, err.Error()), err
}
}
ps, err := sourceignore.ReadIgnoreFile(path, nil)
if err != nil {
return sourcev1.BucketNotReady(bucket, sourcev1.BucketOperationFailedReason, err.Error()), err
}
// In-spec patterns take precedence
if bucket.Spec.Ignore != nil {
ps = append(ps, sourceignore.ReadPatterns(strings.NewReader(*bucket.Spec.Ignore), nil)...)
}
matcher := sourceignore.NewMatcher(ps)

// download bucket content
for object := range s3Client.ListObjects(ctxTimeout, bucket.Spec.BucketName, minio.ListObjectsOptions{
Recursive: true,
UseV1: s3utils.IsGoogleEndpoint(*s3Client.EndpointURL()),
}) {
if object.Err != nil {
err = fmt.Errorf("listing objects from bucket '%s' failed: %w", bucket.Spec.BucketName, object.Err)
return sourcev1.BucketNotReady(bucket, sourcev1.BucketOperationFailedReason, err.Error()), err
}

if strings.HasSuffix(object.Key, "/") || object.Key == sourceignore.IgnoreFile {
continue
}

if matcher.Match(strings.Split(object.Key, "/"), false) {
continue
if bucket.Spec.Provider == sourcev1.GoogleBucketProvider {
sourceBucket, err = r.reconcileWithGCP(ctx, bucket, tempDir)
if err != nil {
return sourceBucket, err
}

localPath := filepath.Join(tempDir, object.Key)
err := s3Client.FGetObject(ctxTimeout, bucket.Spec.BucketName, object.Key, localPath, minio.GetObjectOptions{})
} else {
sourceBucket, err = r.reconcileWithMinio(ctx, bucket, tempDir)
if err != nil {
err = fmt.Errorf("downloading object from bucket '%s' failed: %w", bucket.Spec.BucketName, err)
return sourcev1.BucketNotReady(bucket, sourcev1.BucketOperationFailedReason, err.Error()), err
return sourceBucket, err
}
}

revision, err := r.checksum(tempDir)
if err != nil {
return sourcev1.BucketNotReady(bucket, sourcev1.StorageOperationFailedReason, err.Error()), err
Expand Down Expand Up @@ -315,7 +265,177 @@ func (r *BucketReconciler) reconcileDelete(ctx context.Context, bucket sourcev1.
return ctrl.Result{}, nil
}

func (r *BucketReconciler) auth(ctx context.Context, bucket sourcev1.Bucket) (*minio.Client, error) {
// reconcileWithGCP handles getting objects from a Google Cloud Platform bucket
// using a gcp client
func (r *BucketReconciler) reconcileWithGCP(ctx context.Context, bucket sourcev1.Bucket, tempDir string) (sourcev1.Bucket, error) {
log := logr.FromContext(ctx)
gcpClient, err := r.authGCP(ctx, bucket)
if err != nil {
err = fmt.Errorf("auth error: %w", err)
return sourcev1.BucketNotReady(bucket, sourcev1.AuthenticationFailedReason, err.Error()), err
}
defer gcpClient.Close(log)

ctxTimeout, cancel := context.WithTimeout(ctx, bucket.Spec.Timeout.Duration)
defer cancel()

exists, err := gcpClient.BucketExists(ctxTimeout, bucket.Spec.BucketName)
if err != nil {
return sourcev1.BucketNotReady(bucket, sourcev1.BucketOperationFailedReason, err.Error()), err
}
if !exists {
err = fmt.Errorf("bucket '%s' not found", bucket.Spec.BucketName)
return sourcev1.BucketNotReady(bucket, sourcev1.BucketOperationFailedReason, err.Error()), err
}

// Look for file with ignore rules first.
path := filepath.Join(tempDir, sourceignore.IgnoreFile)
if err := gcpClient.FGetObject(ctxTimeout, bucket.Spec.BucketName, sourceignore.IgnoreFile, path); err != nil {
if err == gcp.ErrorObjectDoesNotExist && sourceignore.IgnoreFile != ".sourceignore" {
return sourcev1.BucketNotReady(bucket, sourcev1.BucketOperationFailedReason, err.Error()), err
}
}
ps, err := sourceignore.ReadIgnoreFile(path, nil)
if err != nil {
return sourcev1.BucketNotReady(bucket, sourcev1.BucketOperationFailedReason, err.Error()), err
}
// In-spec patterns take precedence
if bucket.Spec.Ignore != nil {
ps = append(ps, sourceignore.ReadPatterns(strings.NewReader(*bucket.Spec.Ignore), nil)...)
}
matcher := sourceignore.NewMatcher(ps)
objects := gcpClient.ListObjects(ctxTimeout, bucket.Spec.BucketName, nil)
// download bucket content
for {
object, err := objects.Next()
if err == gcp.IteratorDone {
break
}
if err != nil {
err = fmt.Errorf("listing objects from bucket '%s' failed: %w", bucket.Spec.BucketName, err)
return sourcev1.BucketNotReady(bucket, sourcev1.BucketOperationFailedReason, err.Error()), err
}

if strings.HasSuffix(object.Name, "/") || object.Name == sourceignore.IgnoreFile {
continue
}

if matcher.Match(strings.Split(object.Name, "/"), false) {
continue
}

localPath := filepath.Join(tempDir, object.Name)
if err = gcpClient.FGetObject(ctxTimeout, bucket.Spec.BucketName, object.Name, localPath); err != nil {
err = fmt.Errorf("downloading object from bucket '%s' failed: %w", bucket.Spec.BucketName, err)
return sourcev1.BucketNotReady(bucket, sourcev1.BucketOperationFailedReason, err.Error()), err
}
}
return sourcev1.Bucket{}, nil
}

// reconcileWithMinio handles getting objects from an S3 compatible bucket
// using a minio client
func (r *BucketReconciler) reconcileWithMinio(ctx context.Context, bucket sourcev1.Bucket, tempDir string) (sourcev1.Bucket, error) {
s3Client, err := r.authMinio(ctx, bucket)
if err != nil {
err = fmt.Errorf("auth error: %w", err)
return sourcev1.BucketNotReady(bucket, sourcev1.AuthenticationFailedReason, err.Error()), err
}

ctxTimeout, cancel := context.WithTimeout(ctx, bucket.Spec.Timeout.Duration)
defer cancel()

exists, err := s3Client.BucketExists(ctxTimeout, bucket.Spec.BucketName)
if err != nil {
return sourcev1.BucketNotReady(bucket, sourcev1.BucketOperationFailedReason, err.Error()), err
}
if !exists {
err = fmt.Errorf("bucket '%s' not found", bucket.Spec.BucketName)
return sourcev1.BucketNotReady(bucket, sourcev1.BucketOperationFailedReason, err.Error()), err
}

// Look for file with ignore rules first
// NB: S3 has flat filepath keys making it impossible to look
// for files in "subdirectories" without building up a tree first.
path := filepath.Join(tempDir, sourceignore.IgnoreFile)
if err := s3Client.FGetObject(ctxTimeout, bucket.Spec.BucketName, sourceignore.IgnoreFile, path, minio.GetObjectOptions{}); err != nil {
if resp, ok := err.(minio.ErrorResponse); ok && resp.Code != "NoSuchKey" {
return sourcev1.BucketNotReady(bucket, sourcev1.BucketOperationFailedReason, err.Error()), err
}
}
ps, err := sourceignore.ReadIgnoreFile(path, nil)
if err != nil {
return sourcev1.BucketNotReady(bucket, sourcev1.BucketOperationFailedReason, err.Error()), err
}
// In-spec patterns take precedence
if bucket.Spec.Ignore != nil {
ps = append(ps, sourceignore.ReadPatterns(strings.NewReader(*bucket.Spec.Ignore), nil)...)
}
matcher := sourceignore.NewMatcher(ps)

// download bucket content
for object := range s3Client.ListObjects(ctxTimeout, bucket.Spec.BucketName, minio.ListObjectsOptions{
Recursive: true,
UseV1: s3utils.IsGoogleEndpoint(*s3Client.EndpointURL()),
}) {
if object.Err != nil {
err = fmt.Errorf("listing objects from bucket '%s' failed: %w", bucket.Spec.BucketName, object.Err)
return sourcev1.BucketNotReady(bucket, sourcev1.BucketOperationFailedReason, err.Error()), err
}

if strings.HasSuffix(object.Key, "/") || object.Key == sourceignore.IgnoreFile {
continue
}

if matcher.Match(strings.Split(object.Key, "/"), false) {
continue
}

localPath := filepath.Join(tempDir, object.Key)
err := s3Client.FGetObject(ctxTimeout, bucket.Spec.BucketName, object.Key, localPath, minio.GetObjectOptions{})
if err != nil {
err = fmt.Errorf("downloading object from bucket '%s' failed: %w", bucket.Spec.BucketName, err)
return sourcev1.BucketNotReady(bucket, sourcev1.BucketOperationFailedReason, err.Error()), err
}
}
return sourcev1.Bucket{}, nil
}

// authGCP creates a new Google Cloud Platform storage client
// to interact with the storage service.
func (r *BucketReconciler) authGCP(ctx context.Context, bucket sourcev1.Bucket) (*gcp.GCPClient, error) {
var client *gcp.GCPClient
var err error
if bucket.Spec.SecretRef != nil {
secretName := types.NamespacedName{
Namespace: bucket.GetNamespace(),
Name: bucket.Spec.SecretRef.Name,
}

var secret corev1.Secret
if err := r.Get(ctx, secretName, &secret); err != nil {
return nil, fmt.Errorf("credentials secret error: %w", err)
}
if err := gcp.ValidateSecret(secret.Data, secret.Name); err != nil {
return nil, err
}
client, err = gcp.NewClient(ctx, option.WithCredentialsJSON(secret.Data["serviceaccount"]))
if err != nil {
return nil, err
}
} else {
client, err = gcp.NewClient(ctx)
if err != nil {
return nil, err
}
}
return client, nil

}

// authMinio creates a new Minio client to interact with S3
// compatible storage services.
func (r *BucketReconciler) authMinio(ctx context.Context, bucket sourcev1.Bucket) (*minio.Client, error) {
opt := minio.Options{
Region: bucket.Spec.Region,
Secure: !bucket.Spec.Insecure,
Expand Down
2 changes: 1 addition & 1 deletion docs/api/source.md
Original file line number Diff line number Diff line change
Expand Up @@ -2032,4 +2032,4 @@ string
<p>Source interface must be supported by all API types.</p>
<div class="admonition note">
<p class="last">This page was automatically generated with <code>gen-crd-api-reference-docs</code></p>
</div>
</div>
2 changes: 1 addition & 1 deletion docs/spec/v1alpha1/buckets.md
Original file line number Diff line number Diff line change
Expand Up @@ -231,4 +231,4 @@ Wait for ready condition:

```bash
kubectl -n gitios-system wait bucket/podinfo --for=condition=ready --timeout=1m
```
```
Loading